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
This commit is contained in:
fy59 2026-08-30 22:17:51 +02:00
parent 663176f884
commit 599be97fbe
61 changed files with 15491 additions and 714 deletions

View file

@ -49,7 +49,7 @@ or persistence format.
Historical references to older Project DB versions remain valid when they
describe the actual historical contract or migration path. Do not rewrite
legitimate v16/v17/v18/v19 history merely because v20 is current.
legitimate v16/v17/v18/v19 history merely because v22 is current.
When a ticket declares `NO_NEW_SUBSYSTEM`, do not introduce an unrelated:
@ -552,7 +552,7 @@ Persistence changes require explicit attention to:
For Project DB:
- preserve current v20 semantics unless a ticket explicitly authorizes a schema
- preserve current v22 semantics unless a ticket explicitly authorizes a schema
change;
- schema-version changes require explicit human authorization;
- migrations must be additive unless a different migration is explicitly

View file

@ -2,26 +2,61 @@
## Statut
**PASS / FROZEN — INTERNAL_PARALLELISM_COMPUTE_RESOURCES_V1.** Ce document
décrit le contrat validé pour `features.extract`, `features.extract.sift`,
`visual_index.update`, `candidate_pair.generate` et `matcher.run`. Ce gel porte
sur le parallélisme interne borné, les réservations Governor et les sorties
canoniques ; il ne transforme pas les résultats de microbenchmarks GPU en une
preuve de débit durable sur corpus sous pression hôte.
**INTERNAL_PARALLELISM_COMPUTE_RESOURCES_V1 — PASS / FROZEN.**
**COMPUTE_GOVERNOR_V2 — PASS / FROZEN.**
**ORB_VULKAN_ASYNC_EXECUTION — PASS / FROZEN.**
Ce document décrit le contrat validé pour `features.extract`,
`features.extract.sift`, `visual_index.update`, `candidate_pair.generate` et
`matcher.run`. Le gel v1 porte sur le parallélisme interne borné, les
réservations Governor et les sorties canoniques. Il ne préjuge pas du choix
opérationnel CPU/Vulkan ajouté par la tranche v2.
## Frontière runtime
La Task Queue conserve un seul callback actif. Une Task peut exploiter à
l'intérieur de ce callback les `cpu_threads` effectivement admis par le
Resource Governor. Ce parallélisme interne ne crée ni pool global, ni deuxième
scheduler, ni file de travail persistante. L'affinité CPU éventuelle appartient
au lanceur ou à l'hôte : la bibliothèque ne choisit et ne persiste aucun ID de
CPU.
scheduler, ni file de travail persistante. Le Governor dérive désormais un
masque privé depuis l'affinité permise et la topologie package/core Linux. Il
réserve des coeurs physiques complets, frères SMT inclus, en ordre décroissant
package/core. Sur l'hôte unrestricted validé, cela donne `0-5,8-13` pour le
calcul lourd et `6,7,14,15` pour le desktop, l'audio et l'interaction. Un masque
caller déjà borné à la capacité de calcul est repris sans seconde réserve ; si
topologie ou affinité manque, seul le budget portable est conservé et aucun ID
n'est inventé.
L'unique worker Queue applique et vérifie son propre masque (`pid=0`) avant les
callbacks ; le creator/main/TUI reste inchangé et les enfants créés depuis le
worker héritent normalement de son affinité. Aucun TID auxiliaire énuméré n'est
passé à `sched_getaffinity()` ou `sched_setaffinity()` : retenir un
`PIDFD_THREAD` ne réserve pas le numéro TID consommé par ces interfaces. Avant
toute création de pthread applicatif et toute initialisation Vulkan, le
démarrage établit donc `MESA_SHADER_CACHE_DISABLE=true`. L'absence de variable
prend ce défaut sûr et une valeur explicite exacte `true` ou `1` est respectée ;
une valeur explicite fausse ou malformée est préservée mais le démarrage est
refusé. Sur la 780M validée, cette politique supprime les helpers de cache
`*:disk$0` qui élargissaient leur masque, et les threads runtime restants
conservent le compute-pool hérité. Elle est inoffensive hors Mesa, n'est ni
persistée ni scientifique, et son état/sa raison sont diagnostiqués sans faux
indicateur de recontrainte auxiliaire.
Le backend Vulkan possède en plus une barrière tardive non mutante : avant son
premier appel Mesa, une requête non vide exige la valeur exacte `true` ou `1`.
Absent, faux ou malformé produit un backend `UNAVAILABLE` mémorisé et aucune
sortie partielle. `backend_info` et les requêtes vides restent non initialisants.
Cette défense couvre les consumers publics ou de feasibility qui ne traversent
pas la Queue ; seuls leurs `main` autonomes peuvent prendre le défaut sûr avant
tout pthread.
Le compute-pool borne `cpu_threads` à l'admission. Un échec d'application est
diagnostiqué sans altérer Task, Queue, durabilité ou science. Cette couture v2
est **PASS / FROZEN**, sans persistance ni ABI publique. Elle complète le gel
v1 sans le redéfinir.
## Audit GPU — 29 août 2026
Cet audit est une constatation de capacité et une recommandation de périmètre ;
il n'ajoute aucun backend ni aucune politique du Governor. La machine contrôlée
Cet audit est une constatation de capacité et une recommandation de périmètre.
La tranche v2 en tire une politique GPU-first seulement pour un backend validé,
déterministe et mesurément supérieur. La machine contrôlée
expose `AMD Radeon 780M Graphics (RADV PHOENIX)`, iGPU Vulkan API 1.4.354 sous
Mesa 26.2.1. OpenCV 5.0.0 y indique le chargement dynamique de Vulkan et
d'OpenCL, avec TBB comme framework parallèle ; ses modules CUDA ne sont pas
@ -29,7 +64,11 @@ disponibles. L'outil `clinfo` n'était pas installé : cette absence ne prouve n
ne valide une exécution OpenCL. Elle ne constitue donc pas une couture GPU
utilisable par Lardon3D.
La 780M est UMA. Toute mémoire de travail GPU, toute double résidence
La 780M est UMA. Les fichiers amdgpu de l'hôte publient un petit aperture VRAM
de 512 Mio mais un GTT système de 7 986 020 352 octets ; Hardware Profile garde
le premier comme capacité de payload observable et le classe shared d'après ces
preuves bornées, sans liste de device IDs. Toute mémoire de travail GPU, toute
double résidence
CPU/GPU et tout staging host-visible consomment la RAM hôte et doivent être
comptés une seule fois par le Resource Governor, conformément à la règle UMA
du [Resource Boundary](resource_boundary.md#selected-gpu) ; ils ne créent pas
@ -43,7 +82,7 @@ transfert/synchronisation à mesurer : l'UMA ne les rend pas gratuites.
| `features.extract` (ORB/SIFT) | Extraction OpenCV CPU ; aucune couture GPU de production. | Aucune API d'extraction GPU n'est validée. Les indicateurs OpenCV Vulkan/OpenCL dynamiques ne fournissent pas à eux seuls un backend d'extraction ; CUDA est indisponible dans ce build. | Il faudrait prouver l'algorithme, les keypoints, descripteurs, ordre et Feature File produits. Les descripteurs et leurs buffers devraient être résidents ou stagés en RAM UMA ; le transfert et la synchronisation risquent de dominer les images bornées. Bénéfice non mesuré, complexité élevée. | Rester CPU. Une étude ne peut commencer qu'avec une API/backend concret et une preuve d'équivalence. |
| `visual_index.update` | Construction et publication CPU ; aucune couture GPU. | Aucun backend/API GPU validé. | Les postings, leur compaction, tri total et publication déterministe sont aujourd'hui CPU. Un backend devrait préserver exactement `table_id,key24,feature_set_id,feature_index`, le segment, SHA-256 et les memberships ; il ajouterait double résidence UMA et synchronisation pour un bénéfice non mesuré. Complexité élevée. | Rester CPU ; aucun chantier GPU n'est justifié par cet audit. |
| `candidate_pair.generate` | Requête Visual Index, top-K et publication CPU ; aucune couture GPU. | Aucun backend/API GPU validé. | Les scores, top-K, tie-breaks, normalisation et ordre de publication sont canoniques. Une accélération devrait rendre ces résultats identiques malgré les accès DB et les petits résultats bornés ; copies/synchronisations UMA et l'accès persistant réduisent le bénéfice attendu. Bénéfice non mesuré, complexité élevée. | Rester CPU ; ne pas introduire un backend GPU ou une seconde politique de sélection. |
| `matcher.run` | BFMatcher CPU ; couture existante limitée au backend Vulkan ORB sériel. Le mode parallèle CPU ne réserve ni n'utilise le GPU. | Vulkan compute existant pour ORB/Hamming top-2 uniquement. Aucun chemin Vulkan/OpenCL/CUDA validé pour SIFT/RootSIFT. | ORB Vulkan déjà prouve une parité top-2 et Match File complète avec le CPU, ce qui permet l'identité persistante commune. Les dispatchs restent soumis aux buffers descriptors et à la synchronisation UMA. SIFT/RootSIFT Vulkan n'est pas équivalent sur égalités adversariales et reste CPU. | Conserver le chemin ORB Vulkan sériel explicitement admis et le CPU parallèle séparé. Ne pas étendre à SIFT/RootSIFT ni fusionner les modes sans nouvelle validation. |
| `matcher.run` | BFMatcher CPU jusqu'à 12 participants ; backend Vulkan ORB exact en conversion begin/finish backend-owned. | Vulkan compute existant pour ORB/Hamming top-2 uniquement. Aucun chemin Vulkan/OpenCL/CUDA validé pour SIFT/RootSIFT. | ORB Vulkan prouve la parité top-2 et Match File complète avec le CPU. La 780M UMA impose de compter buffers et staging une fois en RAM hôte ; SIFT/RootSIFT restent CPU. | Workload GPU primaire validé et supérieur : AUTO GPU-first, CPU fallback. Ne pas étendre à SIFT/RootSIFT ni changer l'identité persistante. |
L'attente « sortie identique » est une exigence de preuve, non une présomption
attachée au GPU. Un futur backend ne peut partager une identité, un fingerprint
@ -57,14 +96,19 @@ prouvée à cette règle d'identité commune.
### Décision GPU finale — Radeon 780M
La validation de production de `matcher.run` distingue l'identité scientifique
de son mode opérationnel. Les API historiques créent toujours le Matcher CPU
parallèle (`CPU=8`, `GPU=0`). L'API additive de mode explicite peut créer ORB
Vulkan seulement avant admission, avec `CPU=1`, un slot GPU et 640 Kio UMA.
Cette réservation couvre les buffers persistants A (256 Kio), B (256 Kio) et
top-2 (128 Kio); les 10 Mio de stage Matcher restent la mémoire CPU par paire.
Une panne de dispatch reprend entièrement le top-2 CPU avant toute publication;
elle ne publie jamais une sortie GPU partielle ni ne change fingerprint,
identité Match Result ou SHA du Match File.
de son mode opérationnel. La production normale ORB crée une enveloppe AUTO
CPU12/GPU0 et ORB Vulkan CPU1/GPU1; les API explicites conservent une seule
forme depth 1. Sa signature durable `MIXED` reste CPU12/GPU0 et décrit cette
politique, tandis que l'override CPU persiste la classe `CPU`. Chaque slot vaut
640 Kio: buffers A (256 Kio), B (256 Kio) et top-2 (128 Kio). AUTO normal
réserve 640 Kio à inflight 1. La capacité privée de sûreté/benchmark peut
réserver 1,25 Mio à depth 2. Le backend ne mappe aucun slot avant initialisation,
retient exactement la capacité admise pendant la séquence et libère le second
avant l'admission depth 1 suivante; `backend_info` rapporte cette rétention
réelle. Les 10 Mio de stage restent la mémoire CPU par paire.
Une panne de dispatch reprend entièrement le top-2 CPU avant
toute publication ; elle ne publie jamais une sortie GPU partielle et ne
change ni fingerprint, ni identité Match Result, ni SHA du Match File.
Sur l'hôte contrôlé (`AMD Radeon 780M Graphics (RADV PHOENIX)`, Vulkan 1.4.354,
Mesa 26.2.1), le backend a créé le device et exécuté les dispatchs réels. Les
@ -78,20 +122,22 @@ mesures de kernel chaud, distinctes du coût Task/SQLite/checkpoint, sont :
La parité top-2, Match File et publication durable est couverte par une Task
Queue réelle à 769 × 769 descriptors, par une répétition déterministe et par
le fallback forcé. La reprise accepte seulement les signatures entières CPU8,
Vulkan CPU1 et les deux signatures CPU12 historiques; celles-ci sont
normalisées éphémèrement vers leur forme courante avant admission. Une signature voisine
est rejetée. La configuration portable `-Dvulkan_orb=disabled` refuse le mode
Vulkan avant allocation de Task ID et conserve le Matcher CPU.
le fallback forcé. La forme explicite CPU12/GPU0, la signature AUTO `MIXED`
CPU12/GPU0 et la forme Vulkan CPU1/GPU1 utilisent des lots `1..12` et 10 Mio
par paire. La reprise accepte
en plus seulement les anciennes signatures entières CPU8/GPU0 et Vulkan
CPU1/GPU1 à lot maximal 8, puis les formes CPU12 antérieures au coût par paire.
Elles sont des signatures historiques de récupération, normalisées
éphémèrement avant admission ; une forme voisine est rejetée. La configuration
portable `-Dvulkan_orb=disabled` refuse Vulkan et conserve le fallback CPU.
`HOST_PRESSURE_CONTAMINATED=TRUE` pour une comparaison de débit de corpus : au
moment de l'audit, `MemAvailable` était ~7,6 Gio, 7,6 Gio de swap étaient en
usage, même si la PSI mémoire courante était nulle. Il n'existe donc pas de
mesure défendable CPU t1/t6/t12 contre corpus Vulkan dans cette tranche. Le
Matcher CPU durable actuel plafonne en outre à huit participants utiles; un
CPU t12 ne serait pas une comparaison de Task valide. Les chiffres ci-dessus
sont volontairement limités au kernel commun et ne sélectionnent pas une
politique AUTO ou le backend par défaut.
Le gel v1 ne revendiquait pas de comparaison de débit corpus saine sous la
pression hôte alors observée. L'évidence directe apportée à la tranche v2
établit désormais ORB Vulkan comme backend déterministe, exact et mesurément
supérieur pour ce hot path. La politique canonique v2 est donc GPU-first pour
ORB Matcher lorsque son contrat GPU/UMA est admissible, avec fallback CPU.
Cette décision opérationnelle ne rouvre pas le gel scientifique v1 et ne
s'étend pas aux trois domaines GPU rejetés ci-dessous.
Les classifications finales sont :
@ -100,29 +146,42 @@ Les classifications finales sont :
| `candidate_pair.generate` | `CANDIDATE_GPU=REJECTED_WITH_MEASURED_REASON` | Le coût est Visual Index, filtrage, branchement et publication SQLite ordonnée; aucune primitive GPU existante ne préserve ces identités et le CPU parallèle Candidate a déjà démontré 7,114× à t12. |
| `features.extract` | `FEATURE_GPU=REJECTED_WITH_MEASURED_OR_IMPLEMENTATION_EVIDENCE` | OpenCV 5.0.0 installé n'expose aucun ORB/SIFT Vulkan/OpenCL utilisable, CUDA est absent, et aucun seam existant ne peut prouver les Feature Files byte-identiques. |
| `visual_index.update` | `VISUAL_INDEX_GPU=REJECTED_WITH_MEASURED_REASON` | Postings, tri total, SHA et publication déterministe sont CPU; aucun kernel GPU borné existant ne couvre cette frontière. |
| `matcher.run` ORB | `MATCHER_GPU=EXISTING_BACKEND_VALIDATED_BUT_CPU_PREFERRED` | Le backend est réellement admis, dispatché et exact, mais la pression hôte et l'absence d'une comparaison de débit Task/corpus saine interdisent de préférer Vulkan au CPU parallèle comme défaut. |
| `matcher.run` ORB | `MATCHER_GPU=EXISTING_BACKEND_VALIDATED_AND_PREFERRED` | Le backend est exact, déterministe et mesurément supérieur pour le hot path validé. AUTO le préfère quand GPU/UMA sont admis et conserve le CPU en fallback. |
Cette dernière classification ne rejette pas le backend : ORB Vulkan reste un
mode explicite validé. Elle interdit seulement de présenter les microbenchmarks
comme une preuve que le mode sériel GPU bat le Matcher CPU parallèle dans un
corpus durable complet.
Le comportement de production normal est `AUTO`, choisi par le Governor à
chaque admission de séquence. Les modes CPU/Vulkan explicites restent des
overrides de debug, benchmark et reproductibilité. L'enveloppe privée
CPU/Vulkan, le fallback CPU complet et les diagnostics bornés sont
**PASS / FROZEN** dans les limites validées.
## Extraction OpenCV
Le processus configure une seule fois la limite de threads interne OpenCV,
avant la création de la Queue, au budget interactif audité
`min(12, logical_cpu_count - system_cpu_reserve)`. Les Tasks ORB et SIFT demandent
le plafond douze au Governor, qui réduit l'admission à ce même budget ; la
réservation vit pendant l'exécution bornée d'une
image et est libérée avec la Task. OpenCV possède son fan-out interne, tandis
que la Queue reste propriétaire de l'unique callback actif.
Le processus conserve une limite OpenCV globale. Sous l'unique callback Queue,
RAW et Photo Quality appliquent/restaurent CPU1. ORB, SIFT et RootSIFT
Extraction appliquent/restaurent exactement le `cpu_threads` immutable admis
dans `1..min(12, compute_pool)`. Matcher applique OpenCV1 à l'intérieur de sa
propre fenêtre et utilise les participants Task admis autour de cette
primitive. La réservation vit pendant l'exécution bornée et aucun second pool
runtime n'est créé.
Le nombre admis est une politique de ressources, pas une identité scientifique.
L'audit OpenCV 5.0.0 contrôlé à 1/2/4/8/12 threads obtient des Feature Files
ORB v1 et SIFT v2 byte-identiques. Aucun fingerprint, format ou schéma n'est
modifié. Les utilisateurs imbriqués d'OpenCV qui réduisent temporairement cette
limite doivent restaurer sa valeur avant de libérer leur réservation ; ils ne
peuvent pas faire varier la configuration pendant une extraction concurrente.
Les tests OpenCV 5.0.0 à 1/2/4/8/12 threads obtiennent les mêmes keypoints,
descripteurs et métriques ORB, SIFT et RootSIFT. Aucun fingerprint, format ou
schéma n'est modifié. Les utilisateurs imbriqués d'OpenCV ne peuvent pas faire
varier cette configuration process-wide pendant une extraction. Le callback
assure donc qu'une admission CPU est réellement consommée, sans confondre cette
dimension avec le lot admis.
Le Governor slow-start les dimensions CPU validées selon `1/2/4/8/12`. Chaque
palier utilise deux observations et exige au moins 5 % de débit durable en plus.
Un seul essai CPU ou lot est actif à la fois ; après plafonnement ou refus CPU,
un kind dont le lot est adaptable peut seulement alors explorer le lot. Les
callbacks atomiques ORB/SIFT/RootSIFT publient un item seulement après extraction
et publication durable propre ; READY, `ALREADY_PRESENT` ou
`PUBLISHED_NOT_DURABLE` publie une observation zéro qui ne fait progresser aucun
essai. Visual Index suit la même règle par
segment, tandis que Candidate et Matcher observent chaque séquence. Les formes
fixes restent fixes mais conservent un diagnostic d'admission Governor-owned.
Pour Candidate Generation, l'estimation demande jusqu'à douze threads CPU et
un slot I/O. Le callback de Queue compte comme un de ces threads ; il crée donc
@ -246,29 +305,126 @@ de la Task.
Les sources Matcher sont les Candidate Pairs durables, pagées en ordre
croissant de `candidate_pair_id`. Une fenêtre contient au plus
`2 * cpu_threads` paires et jamais plus de huit. Le callback Queue compte comme
`2 * cpu_threads` paires et jamais plus de douze. Le callback Queue compte comme
un participant et crée donc au plus `cpu_threads - 1` enfants ; tous sont
joints avant publication, libération de réservation ou `sequence_break`.
Chaque participant calcule un Match File temporaire privé. OpenCV est configuré
à un thread interne pendant la séquence afin que `BFMatcher` ne crée pas un
second fan-out sous les participants admis. Le backend Vulkan ORB partagé reste
utilisé uniquement par un mode déclaré sériel avant admission, dont l'estimation
réserve un thread CPU et le GPU. Le mode parallèle reste CPU-only même si le
Governor ne lui accorde finalement qu'un participant ; il ne sélectionne donc
jamais tardivement une ressource non réservée. Ce choix runtime ne modifie ni le
fingerprint, ni Lowe ratio, ni les correspondances brutes, ni l'identité Match
Result.
Chaque participant CPU calcule un Match File temporaire privé. OpenCV reste à
un thread interne pendant cette séquence afin que `BFMatcher` ne crée pas un
second fan-out. Le Governor sélectionne avant admission soit la forme
CPU1..12/GPU0, soit ORB Vulkan CPU1/GPU1, 640 Kio UMA et inflight 1.
Les participants
CPU sont décrits par `cpu_threads`; `helpers` reste zéro. Le fallback CPU
complet est choisi avant la séquence si GPU/UMA n'est pas admis ; une panne de
backend ou une paire sous le seuil Vulkan pendant une séquence reprend aussi le
top-2 CPU exact dans la réservation immutable. Le diagnostic distingue backend
sélectionné et backend réel, y compris plusieurs paires complètes CPU/Vulkan.
Ce choix ne
modifie ni fingerprint, ni Lowe ratio, ni correspondances brutes, ni identité
Match Result.
Les APIs historiques sélectionnent toujours le mode CPU parallèle ; le mode
ORB Vulkan est demandé par une API additive explicite et n'est jamais activé
par variable d'environnement ou réduction Governor. Sa création exige ORB, un
GPU sélectionné et un backend présent. Après reprise, la forme immutable GPU
peut conserver le fallback CPU exact si le backend runtime n'est plus
disponible. Les signatures courantes complètes sont CPU8/GPU0 et
CPU1/GPU1/640 Kio ; les signatures historiques CPU12 correspondantes sont
normalisées éphémèrement comme un tout avant admission. Toute forme
voisine est rejetée afin de ne pas inférer un mode depuis un champ isolé.
La couture v2 emploie une enveloppe privée et le mode `AUTO` Governor-owned.
CPU/Vulkan explicites restent des overrides de
debug, benchmark et reproductibilité. Une fois une séquence admise, son
contrat est immutable jusqu'à libération ; seul le contrat de la séquence
suivante peut changer. Une Task ORB normale nouvelle persiste la classe
`MIXED` avec ses champs CPU12/GPU0 réels ; elle seule reconstruit AUTO. Les
signatures CPU ORB courantes ou historiques reconstruisent CPU fixe, et les
signatures Vulkan reconstruisent Vulkan fixe. Le build portable conserve
`MIXED` mais expose seulement CPU. Les signatures historiques exactes CPU8,
Vulkan à lot maximal 8 et CPU12 antérieures au coût par paire restent reconnues;
toute forme voisine est rejetée.
Le pipeline Vulkan rolling repose uniquement sur la couture privée interne.
Device/pipeline/layout/cache restent partagés; deux slots maximum dupliquent
command/fence/descriptors/buffers/query et portent chacun un handle
`slot+generation`. Toute fin, sortie invalide, annulation ou erreur nettoie le
handle exact ; un échec d'attente condamne la session. Une instrumentation
bornée prouve sans sommeil `SUBMIT(i) < SUBMIT(i+1) < FINISH(i) <
PUBLICATION_START(i) < PUBLICATION_FINISH(i)` à depth 2, ainsi que les chemins
begin/publish/cancel en échec et la réutilisation. Aucun helper GPU n'est créé.
Le payload mappé suit la capacité de séquence admise : un slot à depth 1, deux
seulement pendant depth 2, sans redimensionnement pending et avec libération du
second avant la prochaine admission. Une génération arrivée à `UINT64_MAX` ne
boucle pas; son slot est retiré définitivement avant toute nouvelle soumission.
Le runner `pre-sfm-real-execution` compile en plus un contrôle de benchmark
synchrone privé. Il force le contrat GPU depth 1 et exécute
`top2 synchrone → postprocess canonique → publication` avant la paire suivante.
Le défaut du runner reste rolling et le défaut Matcher normal reste AUTO ; CPU
ou Vulkan explicites demeurent des overrides. Le contrôle synchrone est absent
du binaire et du comportement de production, n'est pas persisté et ne peut pas
être repris depuis une Task pendante. Il sert uniquement à comparer l'overlap à
sortie identique. Le corpus court retenu démontre +10,27 % pour rolling depth 1
face à ce contrôle, avec digest identique. L'A/B forcé ABBA mesure ensuite
54,661652238 paires/s à depth 1 contre 55,797311953 à depth 2 (+2,077617 %),
sous le deadband 5 %, avec digest identique, quatre séquences de fallback local et zéro
panne/discard dans chaque exécution. Depth 2 est donc
**REJECTED_WITH_MEASURED_REASON** pour AUTO normal; il reste une capacité privée
de sûreté/benchmark; le lifecycle normal est **PASS / FROZEN**.
Le target runner/test compile aussi un contrôle A/B `--matcher-inflight 1|2`,
absent de la production. Pour AUTO rolling, il fixe min=max inflight et batch=2
par défaut. `--matcher-batch 2|4|8|12`, valable seulement avec inflight, fixe
aussi min=max batch dans la même enveloppe privée à capacité Vulkan unique;
l'unique
Governor conserve sélection, réservation UMA et contrat immutable. Un budget
GPU nul est refusé avant ouverture du Project, et une indisponibilité
GPU/backend/mémoire ne peut jamais sélectionner CPU à la place. Synchronous
accepte seulement 1.
Cette dimension opérationnelle n'est ni persistée ni scientifique, ne s'applique
pas à une Task pendante et est rapportée explicitement dans le JSON. Les
fixtures comparent rolling 1, rolling 2 et synchrone 1 sur des Projects neufs
avec sorties exactes. Une paire localement inéligible peut toujours produire sa
preuve CPU complète. Matcher l'attribue exactement une fois après publication
durable à un compteur d'items local-ineligible, backend-failure ou other; le
travail CPU sélectionné n'est pas un fallback. Les compteurs de séquences
et l'apprentissage de débit restent séparés : si la paire suivante échoue en
calcul/publication ou si la Task est annulée, le préfixe durable reste compté,
sans transformer la séquence avortée en observation valide. La cause locale
d'une paire n'est jamais écrasée par une panne begin/finish voisine.
Ils restent diagnostiques, mais seul le nombre d'items localement inéligibles doit
être identique entre cohortes de lots différents. Un item backend-failure/other,
un discard, un slot pending, un contrat différent ou toute admission CPU rend
`experiment_valid=false` et fait échouer la Task de benchmark après checkpoint
de l'éventuel fallback déjà durable. Les logs préliminaires batch 2/4 retenus
montrent quatre contre trois séquences locales pour les mêmes 4113 IDs/digest :
ils prouvent seulement que l'ancien comparateur dépendait du regroupement et ne
participent pas à la décision. La matrice item-valide
`forced-batch{2,4,8,12}-items{,-b}.stdout.jsonl` donne respectivement
54,180767704, 66,094373197, 74,784998723 et 76,755814095 paires/s, avec six
items locaux, zéro panne/autre et le même digest dans chaque run. Les gains de
palier sont +21,988624373 %, +13,148812987 % et +2,635308425 %. Le dernier est
sous le deadband 5 % : AUTO normal conserve `BATCH_MAX_USEFUL=8`, tandis que
batch 12 reste une capacité privée sûre
`REJECTED_WITH_MEASURED_REASON`.
Le contrôle de production sans override est conservé dans
`short-auto-batch8-governor-v2.stdout.jsonl` : la cadence durable complète fait
monter les contrats `1 → 2 → 4 → 8`, puis garde 8 jusqu'au résultat 4113/4113,
à 76,072 paires/s avec digest identique, inflight 1, helpers 0 et zéro panne ou
discard.
La preuve S21 finale sans override conserve exactement ces limites sur
172 741 paires : dernier contrat batch 8/inflight 1/helpers 0, 73,649 résultats
durables/s et zéro panne Vulkan. Un seul épisode YELLOW réduit batch 8 à 1;
après retour GREEN, la progression contrôlée 1 → 2 → 4 → 8 est rejouée.
Depth 2, helpers et batch 12 ne sont jamais admis en production. Le thread
Queue lourd reste sur `0-5,8-13`; `6,7,14,15` restent réservés au desktop.
La création et la reprise AUTO ne sondent plus le backend sur le caller/main.
Elles exposent la capacité depuis les seules métadonnées build/backend/GPU ; le
Governor possède le dimensionnement exact et l'admission UMA sur son snapshot. Le premier
`begin` initialise Vulkan seulement sur le worker Queue déjà contraint, après
établissement pré-pthread de la politique de cache Mesa. Aucun sweep auxiliaire
post-init, latch de Task ou retry de recontrainte par TID n'existe. Une paire
localement inéligible n'initialise pas le backend. Une panne d'initialisation
désactive les admissions GPU AUTO
suivantes, tandis qu'une paire sous le seuil reste une inéligibilité locale et
non une panne.
L'état request-bound distingue un handle soumis de ces deux causes : aucune
paire sans handle ne passe par `finish`. Une panne réelle invalide la
disponibilité Governor avant tout fallback ou sortie précoce ; une inéligibilité
locale conserve cette disponibilité. Les restaurations CPU/Vulkan/historiques
fixes ne peuvent pas écraser l'état établi par une restauration AUTO.
Après jointure, seul le callback propriétaire publie les stages, dans l'ordre
croissant de `candidate_pair_id`, par le chemin atomique Match Store existant.

View file

@ -1,5 +1,11 @@
# 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`,
@ -83,15 +89,16 @@ 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.
**IMPLEMENTATION IN PROGRESS — parallélisme interne P4.** `matcher.run` v1
**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 1/2/4/8 sont séparés par checkpoint et
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 de huit. Le callback Queue
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é.
@ -99,6 +106,15 @@ Le curseur ne suit que le préfixe contigu durable ; le premier échec scelle to
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.
@ -111,21 +127,237 @@ 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. La sélection est
une politique runtime explicite ; le CPU reste le fallback portable. Les APIs
historiques de création/soumission choisissent toujours le mode CPU parallèle.
Les variantes additives `*_with_mode` permettent de demander ORB Vulkan avant
l'admission ; elles refusent SIFT/RootSIFT, un profil sans GPU sélectionné ou
un backend absent. Ce mode immutable demande exactement CPU 1, GPU 1 et
640 Kio GPU/UMA. Un échec Vulkan pendant une exécution déjà admise reprend le
top-2 CPU exact sans changer fingerprint ni résultat. Le contrat détaillé est
décrit dans [vulkan_matcher.md](vulkan_matcher.md).
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.
La reprise reconnaît uniquement quatre estimations complètes : CPU8/GPU0 et
CPU1/GPU1/640 Kio courantes, plus CPU12/GPU0 et CPU12/GPU1/640 Kio
historiques. Les deux formes CPU12 sont normalisées éphémèrement vers leur
forme courante avant admission, sans checkpoint d'estimation seule.
Une forme voisine est rejetée ; aucun champ isolé ne sert à deviner le backend.
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](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

View file

@ -56,11 +56,14 @@ validé. Après crash, la registry statique reconstruit le même `task_id`; l'im
est recommencée, sans micro-checkpoint trompeur. Aucun Feature Set partiel ne
devient READY. Le SHA-256 de l'Image Asset géré est vérifié avant décodage.
L'estimation SIFT demande jusqu'à douze threads CPU ; le Governor réduit ce
plafond à la limite interne OpenCV configurée au démarrage. Elle réserve aussi
L'estimation SIFT demande jusqu'à douze threads CPU. Le réglage OpenCV du
démarrage est une baseline/plafond sûre ; pour chaque séquence, l'unique
callback Queue applique temporairement le compte immuable admis dans `1..12`,
le vérifie puis restaure la baseline sur toute sortie. Une mutation process-wide
concurrente par plusieurs workers n'est pas supportée. La tâche réserve aussi
un slot IO, aucun GPU et environ 1,06 Gio structurels :
image décodée, pyramides OpenCV, candidats et descriptors. Le pic de lot est
zéro. Le Resource Governor admet donc le fan-out OpenCV complet ; la Queue
zéro. Le Resource Governor admet donc le fan-out OpenCV exact ; la Queue
conserve un seul callback actif et ne superpose pas un second pool SIFT.
Le nombre de threads reste opérationnel et absent du fingerprint SIFT/RootSIFT.

View file

@ -38,9 +38,13 @@ chemins de production existants et leur validation finale est terminée.
Les snapshots emploient `CLOCK_MONOTONIC` et leur âge maximal est 1000 ms. Une
capture complète impossible est une erreur opérationnelle, tandis qu'une PSI
ou télémétrie swap optionnelle absente reste inconnue. Le modèle cible un hôte
Linux natif non contraint ; cgroups, limites systemd/RLIMIT, multi-GPU,
monitoring RSS, scratch et stockage externe restent différés.
ou télémétrie swap optionnelle absente reste inconnue. Compute Governor v2
observe maintenant le RSS/HWM courant dans un buffer borné, uniquement comme
diagnostic du processus : il ne le confond ni avec la réservation Task ni avec
un coût attribuable. Le modèle cible un hôte Linux natif non contraint ; cgroups,
limites systemd/RLIMIT, multi-GPU, historique/monitoring RSS long terme,
redimensionnement d'admission depuis le RSS, scratch et stockage externe restent
différés.
## Feature Extraction
@ -49,9 +53,14 @@ publication Feature Store, métadonnées DB, checkpoint terminal et libération
buffer. Le batch vaut donc une image et la granularité de reprise est une image.
Le worker unique et la file bornée fournissent la backpressure actuelle.
OpenCV est configuré une seule fois avant le démarrage des workers. La tâche
réserve le nombre réel de threads OpenCV au lieu d'annoncer artificiellement un
thread pendant qu'une primitive interne en utilise davantage.
Le démarrage configure une baseline et un plafond OpenCV sûrs avant la création
de Queue. L'unique callback lourd applique ensuite temporairement le compte CPU
immuable admis pour sa séquence, dans `1..12`, et restaure la baseline sur toute
sortie, y compris après une mutation suivie d'un échec de vérification. La tâche
réserve donc le nombre réellement appliqué au lieu d'annoncer artificiellement
un thread pendant qu'une primitive interne en utilise davantage. Une mutation
process-wide concurrente par plusieurs workers n'est pas supportée ; Queue
conserve un seul callback actif.
## Matcher
@ -82,12 +91,17 @@ slow-start restent exclusivement décidés par Runtime et Governor.
## GPU et files
La Radeon 780M est UMA : toute mémoire GPU compte aussi comme pression RAM. Un
backend GPU emploie un unique job actif, des dispatchs courts, puis publie avant
de continuer. Vulkan 1.4.354 énumère la 780M RADV et une file compute dédiée. Le
backend ORB top-2 de production possède un contexte lazy réutilisable, 640 Kio
de buffers bornés et un fallback CPU exact. Le CPU reste le fallback portable
si Vulkan est absent, incompatible ou désactivé pour la session.
La Radeon 780M est UMA : toute mémoire GPU compte aussi comme pression RAM.
Vulkan 1.4.354 énumère la 780M RADV et une file compute dédiée. Le backend ORB
top-2 de production possède un contexte lazy réutilisable et jusqu'à deux jobs
privés en vol sur cette file, sans helper hôte. Chaque slot mappé vaut 640 Kio.
Le backend part de zéro et retient exactement un slot après une initialisation
ou séquence AUTO normale depth 1. Il n'alloue le second que sous un contrat
privé de sûreté/benchmark depth 2 déjà admis, puis le libère avant de franchir
la prochaine admission depth 1. Les
publications restent strictement ordonnées et le fallback CPU reste exact. Le
CPU reste le fallback portable si Vulkan est absent, incompatible ou désactivé
pour la session.
La feasibility SIFT/RootSIFT a borné son prototype Vulkan à 8,125 Mio de
payload lazy, mais n'a pas franchi la Gate de production. Le Governor ne réserve

View file

@ -1,13 +1,303 @@
# Resource Governor Lardon3D
## COMPUTE_GOVERNOR_V2 — PASS / FROZEN
**COMPUTE_GOVERNOR_V2 — PASS / FROZEN.**
**ORB_VULKAN_ASYNC_EXECUTION — PASS / FROZEN.** Gate G core reste
**PASS / FROZEN**. Cette évolution opérationnelle ne change ni le contrat
scientifique Matcher gelé, ni les octets Match File, ni l'ordre du curseur, ni
la durabilité. Sa cible est une boucle fermée à `sequence_break()` : le
Governor observe une télémétrie hôte/Task bornée, choisit un contrat pour la
séquence suivante, puis le maintient immutable pendant toute son exécution.
Cette couture privée est gelée ; seule une séquence ultérieure peut recevoir
un autre contrat. Les dimensions retenues, le corpus S21 complet, les suites
portable/Vulkan, les sanitizers et l'audit XHIGH ferment le statut v2.
Sur le profil validé Ryzen 7 8845HS, le Governor lit le masque d'affinité permis
et la topologie package/core Linux. Il réserve des groupes de coeurs physiques
complets, frères SMT inclus, en préférant déterministiquement les plus grands
IDs package/core. Avec le masque unrestricted 015 et une réserve logique de
quatre, il dérive le pool lourd `0-5,8-13` et réserve `6,7,14,15` au desktop
Arch/Sway, à l'audio et à l'interaction ordinaire. Si le caller est déjà limité
à au plus `logical_total - reserve`, son masque permis devient directement le
pool de calcul sans seconde soustraction. Sans affinité/topologie exploitable,
le budget portable `logical_total - reserve` subsiste sans exclusion arbitraire
de frères SMT et l'affinité est diagnostiquée inactive. Cette politique privée
est **PASS / FROZEN** sur le profil validé ; aucun ID CPU n'est persisté ni ne
devient une identité scientifique.
Seul le worker lourd de l'unique Queue applique et relit son propre masque
(`pid=0`) avant les callbacks. Le caller/main/TUI reste unrestricted et aucun
autre processus n'est touché. Un pidfd de thread ne stabilise pas l'identifiant
numérique consommé par `sched_setaffinity(tid)` après la sortie du thread : le
Governor n'énumère ni ne mute donc jamais un TID auxiliaire. À la place,
Lardon3D établit `MESA_SHADER_CACHE_DISABLE=true` avant toute création de pthread
applicatif et avant toute initialisation Vulkan. L'absence de variable prend ce
défaut sûr ; les valeurs explicites exactes `true` et `1` sont conservées. Une
valeur explicite fausse ou malformée n'est pas écrasée et fait échouer le
démarrage, car elle permettrait à Mesa de créer ses helpers de cache disque
observés capables d'élargir leur affinité. La variable est inoffensive pour les
drivers non-Mesa, opérationnelle seulement, non persistée et absente de toute
identité scientifique. Sur le profil 780M contrôlé, les helpers `*:disk$0`
disparaissent ; tous les threads runtime restants observés héritent et gardent
`0-5,8-13`. Les diagnostics privés exposent l'activité de cette politique, la
valeur sûre et sa raison, sans prétendre recontraindre des auxiliaires.
Cette garantie est aussi défensive dans le backend public : toute requête
non vide qui devrait initialiser Vulkan vérifie sans mutation la valeur exacte
`true`/`1` avant le premier appel Mesa. Une valeur absente ou différente rend ce
contexte backend indisponible et retourne `UNAVAILABLE` sans sortie partielle.
La lecture metadata demeure non initialisante. Ainsi un consumer direct qui ne
passe ni par l'application ni par le runner ne contourne pas la réserve CPU.
Le nombre de CPUs du pool borne directement l'admission.
CPU Matcher reste validé dans `1..12`, indépendamment du lot :
`cpu_threads=12, batch_size=1` demeure un contrat valide.
La cible Compute Governor v2 conserve 3 GiB de `MemAvailable` et ne franchit
pas intentionnellement le plancher dur de 2 GiB sur cette classe d'hôte 16 GiB.
Les entrées de pression actives sont `MemAvailable`, les PSI CPU/mémoire/I/O et
les deltas swap-in/swap-out entre observations. L'occupation totale du swap est
un état historique, pas à elle seule une activité récente. Ces objectifs v2 ne
réécrivent pas rétroactivement les constantes Gate G core gelées ci-dessous.
Les observations saines autorisent une croissance lente. Pour une dimension
CPU réellement réductible, la rampe est exactement `1 → 2 → 4 → 8 → 12`,
bornée par l'enveloppe et le compute-pool. Deux séquences établissent d'abord
une référence de débit durable puis ouvrent un essai borné au palier supérieur.
Les dimensions CPU/génériques conservent deux observations d'essai et un gain
d'au moins 5 % avant une nouvelle croissance. Le lot ORB Vulkan normal exige
désormais huit observations pures consécutives pour sa référence puis huit pour
chaque essai; sa décision compare les moyennes bornées au même deadband de 5 %.
Pour Matcher, cette observation est la cadence durable complète de la séquence :
à partir de la deuxième séquence elle commence juste avant la rupture/réadmission
et se termine seulement après calcul, publication owner-only et checkpoint
générique durable. Le temps interne du calcul reste une métrique séparée, mais
ne peut pas masquer le coût de réadmission que le choix du lot doit amortir.
CPU et lot partagent un unique essai : deux dimensions ne changent jamais dans
la même mesure. L'infrastructure privée sait aussi
borner un essai inflight, mais l'enveloppe ORB AUTO normale le fixe maintenant
à 1 après la mesure A/B décrite ci-dessous. Un échantillon rapide isolé ne
décide rien ; sans gain, le plafond revient au dernier palier accepté et s'y
arrête. PSI ou delta swap actif abandonne immédiatement l'essai ; toute
admission encore permise prend lot minimum, inflight minimum et CPU1
avant réservation. Un `WAIT`/`REJECT` Gate G reste inchangé. Après
l'hystérésis `RED → YELLOW → GREEN`, une nouvelle référence
permet de reprendre des essais contrôlés. Les diagnostics distinguent les
essais/gains/refus CPU, inflight et lot, `backend-fallback-hold` et
`pressure-decrease`. En production normale, ORB est inflight 1 et helpers 0.
La télémétrie hôte privée lit, avec capacités fixes et parse strict, les deltas
`/proc/stat` limités au masque du compute-pool, `MemAvailable`, PSI mémoire et
I/O `some`/`full`, les deltas actifs `pswpin`/`pswpout`, RSS/HWM du processus et
le `gpu_busy_percent` de l'unique index DRM retenu par Hardware Profile, sans
scan ni fallback vers une autre carte. Utilisation CPU et GPU
sont exprimées en basis points avec un bit `known`; régression, overflow,
troncature, token malformé ou signal absent donnent `unknown`. La charge globale
n'est pas rebaptisée utilisation du pool et un Task qui utilise son pool admis
n'est pas réduit pour ce seul fait. RSS/HWM reste une observation du processus,
jamais une réservation mémoire Task. Aucun échec de cette capture optionnelle
ne fait échouer l'exécution scientifique.
Pour ORB Vulkan normal, seul le lot `1..8` reste essayable ; inflight est fixé à
1 et helpers à 0. Huit séquences pures saines construisent la référence, puis
huit séquences au palier d'essai sont nécessaires avant acceptation ou refus.
Fallback, travail durable nul et pression sont exclus et réinitialisent la
fenêtre pertinente. Un backend affamé ou un GPU peu occupé sous hôte sain peut
ouvrir cet essai ; `gpu_busy` inconnu ne l'interdit pas lorsque la starvation
backend est observable. Seul le gain moyen de débit durable accepte le palier.
Une occupation GPU plus haute sans gain le fait revenir au dernier contrat
accepté et arrête cette croissance. Un ou deux échantillons, hauts ou bas, ne
décident donc jamais le lot GPU.
La mesure forcée ABBA du même corpus et du même binaire donne 54,661652238
paires/s à depth 1 et 55,797311953 paires/s à depth 2, soit +2,077617 %, sous
le deadband matériel de 5 %. Ces débits combinés valent
`(2 * 4113 * 1e9) / (wall_ns_a + wall_ns_b)`, et non la moyenne des deux débits
par run. Les `wall_ns` bruts sont 75326831673/75162582080 à depth 1 et
73662096698/73764360098 à depth 2; les walls moyens sont donc
75,244706877/73,713228398 s. Les autres moyennes depth 1/depth 2 sont : fence
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 %. Les quatre exécutions ont le digest
`7a9dbc38a23a600379167d55e24836b7acbb22eea25573e7440bdc9e4602b3b3`, quatre
séquences de fallback local par exécution et zéro panne/discard. Conclusion opérationnelle :
`DEPTH_MAX_VALIDATED_SAFETY=2`, mais `DEPTH_MAX_USEFUL=1`; depth 2 est
**REJECTED_WITH_MEASURED_REASON** pour AUTO normal.
Les agrégats retenus sont
`/home/fy59/Documents/Lardon/.real-pre-sfm-2026-08-30/governor-v2-evidence/`
`forced-depth1-a.stdout.jsonl`, `forced-depth1-b.stdout.jsonl`,
`forced-depth2-a.stdout.jsonl` et `forced-depth2-b.stdout.jsonl`.
La Radeon 780M possède un slot GPU et utilise une mémoire UMA : buffers Vulkan,
staging, descripteurs/commandes et readback en vol sont débités exactement une
fois de la RAM hôte. Hardware Profile ne conclut plus « VRAM séparée » au seul
motif qu'amdgpu publie `mem_info_vram_total`. Il conserve la capacité de payload
rapportée, mais classe conservativement shared/UMA lorsqu'un petit aperture
VRAM volé/dédié est accompagné d'un GTT à l'échelle de la RAM système, ou
lorsque cette petite capacité reste incertaine. Sur l'hôte validé, les preuves
exactes sont 512 Mio de VRAM visible et 7 986 020 352 octets de GTT pour environ
16 Gio de RAM. Une classification UMA conservatrice d'un GPU à faible VRAM peut
refuser inutilement une admission ; classer à tort cet iGPU comme mémoire libre
séparée pourrait contourner les cibles 3 Gio/2 Gio et est interdit. La capacité
rapportée ne forme donc aucun second budget VRAM indépendant sur UMA.
La politique canonique v2 est GPU-first lorsqu'un backend est validé,
déterministe et mesurément plus rapide, sous réserve que son contrat GPU/UMA
soit admis. ORB Matcher est aujourd'hui le workload primaire qui satisfait ces
conditions : la frontière top-2 et la sortie complète sont exactes, et
l'évidence contrôlée le classe supérieure au CPU pour ce hot path. CPU demeure
le fallback portable et de panne. Les choix CPU/Vulkan explicites restent des
overrides de debug, benchmark et reproductibilité. La production normale cible
`AUTO`, choisi par le Governor et observable par séquence. Le code courant
implémente ce comportement pour le Matcher ORB normal : GPU validé d'abord,
puis capacité CPU complète si le build, le backend, le GPU ou l'admission UMA
ne sont pas sûrs. La création/reprise AUTO n'initialise ni ne sonde Vulkan sur
le caller : le premier `begin` initialise le driver sur le worker Queue déjà
contraint. Un échec produit une paire CPU complète sans preuve partielle et
désactive l'admission GPU ultérieure lorsqu'il s'agit réellement du backend ;
une paire sous le seuil Vulkan reste une simple inéligibilité, pas une panne.
Le Matcher représente séparément `SUBMITTED`, inéligibilité locale et panne :
il n'appelle `finish` qu'avec le handle soumis correspondant. Une panne réelle
ne reclasse que les requêtes effectivement soumises ou non encore tentées; la
cause locale déjà établie pour une paire voisine reste locale. Elle met la
disponibilité partagée à faux immédiatement, même si le fallback CPU,
l'annulation ou la publication échoue ensuite. Seule une reprise AUTO peut
réétablir l'éligibilité depuis les faits runtime ; les reprises fixes et
historiques ne l'écrasent pas.
Une faute locale avant soumission (lecture Feature, allocation, chemin) ou
après `finish` Vulkan réussi (filtrage, allocation, staging/fsync Match File)
appartient au bucket `other`, jamais à `backend-failure`. La paire est
recalculée entièrement sur CPU; le backend partagé et les successeurs déjà
soumis restent valides. Le booléen privé `backend_fault` n'est vrai que si la
transaction Vulkan `begin` ou `finish` elle-même échoue.
Les API CPU/Vulkan explicites restent fixes. La télémétrie bornée conserve un
dernier diagnostic sérialisé et un état de rampe par kind/backend ; elle
distingue backend sélectionné/réel, contrat complet, pression, mesures hôte,
débit durable et compteurs Matcher/Vulkan. Une couture privée permet un pull
`since(serial)` et un format texte borné ; rien n'est imprimé directement dans
ncurses, persisté ou accumulé en grande histoire. Les callbacks atomiques
Feature/SIFT/RootSIFT enregistrent un item seulement après extraction et
publication durable propre. READY/`ALREADY_PRESENT` réutilisé ou publication non
durable enregistre zéro et n'avance aucun essai. Visual Index applique la même règle par segment ;
Candidate enregistre chaque séquence durable. Un fallback CPU complet d'une
séquence sélectionnée Vulkan annule l'essai et reconstruit ultérieurement une
référence pure ; il n'entraîne jamais la baseline Vulkan.
Le runner opt-in `pre-sfm-real-execution` consomme maintenant cette couture
d'évidence. Ses échantillons JSON sont explicitement qualifiés
`latest-change-coalescing` : le polling peut fusionner des séquences rapides et
ne prétend donc pas les énumérer toutes. En parallèle, le Governor maintient par
kind/backend des compteurs cumulatifs saturants et des extrema de télémétrie de
taille fixe. Le résumé final rapporte ainsi les admissions et séquences
enregistrées, items durables, changements de contrat et sommes
CPU/Vulkan/publication sans histoire non bornée ni double comptage. Ces agrégats
vivent seulement avec l'instance Governor ; ils ne sont ni un budget RSS, ni un
nouveau payload, ni une persistance.
Ils conservent les classifications par séquence et ajoutent les comptes exacts
par item pour les fallbacks Matcher : inéligibilité locale, panne backend ou
raison autre/inconnue. La Task incrémente l'unique compteur de l'item seulement
après publication durable de son fallback CPU complet; une admission CPU
normale n'est pas un fallback. Cet incrément immédiat est séparé du feedback de
fin de séquence : un préfixe déjà durable reste compté si une paire suivante
échoue ou est annulée, sans créer une observation de débit pour la séquence
avortée. Un high-water mark Task borné évite le double comptage in-process; il
n'est ni persisté ni reconstruit au redémarrage. Ces compteurs fixes saturent
avec le drapeau
commun de l'agrégat et, contrairement au nombre de séquences, restent invariants
quand le lot change. Le contrôle matriciel forcé du runner n'expose qu'une
capacité Vulkan aux batch/depth demandés : zéro admission CPU, payload exact,
aucun changement de contrat, panne, discard ou slot pending sont nécessaires à
`experiment_valid=true`. Les items localement inéligibles ne rendent une
comparaison valide que si leurs comptes sont égaux entre cohortes; tout item de
panne ou autre cause échoue fermement.
La matrice forcée item-valide retenue est
`forced-batch{2,4,8,12}-items{,-b}.stdout.jsonl` sous le répertoire d'évidence
ci-dessus. Chaque run publie 4113 paires, six items localement inéligibles, zéro
item backend-failure/other et le digest
`7a9dbc38a23a600379167d55e24836b7acbb22eea25573e7440bdc9e4602b3b3`. Selon
`(2 * 4113 * 1e9) / (wall_ns_a + wall_ns_b)`, les walls bruts
76150272845/75674818393, 61319835797/63138565155,
55148130595/54847191200 et 53446248173/53724786321 ns donnent respectivement
54,180767704, 66,094373197, 74,784998723 et 76,755814095 paires/s. Les gains de
palier sont +21,988624373 %, +13,148812987 % et +2,635308425 %. Le dernier est
sous le deadband 5 % : `BATCH_MAX_VALIDATED_SAFETY=12` reste disponible au
benchmark privé, `BATCH_MAX_USEFUL=8` borne AUTO normal et batch 12 est
**REJECTED_WITH_MEASURED_REASON**. Les anciens
`forced-batch2-current.stdout.jsonl` et `forced-batch4.stdout.jsonl`, comparés
par nombre de séquences au lieu d'items, restent une preuve historique du
comparateur obsolète et ne participent pas à cette décision.
Le run de production sans override
`short-auto-batch8-governor-v2.stdout.jsonl` confirme ensuite la boucle réelle :
contrats `1 → 2 → 4 → 8`, dernier lot 8, inflight 1, helpers 0, 4113 résultats
durables en 54,066973393 s soit 76,072 paires/s, six fallbacks locaux, zéro
panne/discard et le même digest `L3DMRD1`. Le compute-pool contient 12 CPU,
les quatre CPU `6,7,14,15` restent réservés, l'UMA est comptée, le minimum
`MemAvailable` vaut 12 421 971 968 octets et aucun swap-in/out n'est observé.
La preuve S21 finale sans override,
`final-s21-auto.stdout.jsonl`, ferme la boucle sur 172 741 paires : AUTO
sélectionne Vulkan pour les 21 630 admissions, publie 172 741 résultats en
2 345,444485079 s (73,649 paires/s), termine avec batch 8, inflight 1 et
helpers 0, et ne compte aucune panne, aucun discard ni slot pending. La seule
admission classée YELLOW ramène batch 8 à 1; plusieurs séquences GREEN
reconstruisent ensuite la référence et remontent 1 → 2 → 4 → 8, sans
rebond. Le minimum `MemAvailable` est 10 927 390 720 octets, PSI mémoire
maximal 0, swap-in/out maximal 0, GPU busy moyen/médian/maximal 26/27/36 % et
HWM processus maximal 250 658 816 octets. L'UMA admise reste 655 360 octets;
les masques exacts sont compute `0-5,8-13` et reserve `6,7,14,15`. Cette preuve
est opérationnelle : le digest scientifique et la reprise sont documentés par
le contrat Matcher, pas redéfinis ici.
L'ensemble v2 est **PASS / FROZEN**. Le gel porte sur cette architecture et
ses bornes validées, sans rouvrir Gate G ni le Matcher scientifique.
Les décisions GPU négatives existantes restent inchangées : Candidate, Feature
et Visual Index restent CPU; SIFT/RootSIFT Matcher restent BFMatcher L2 CPU. Il
n'est introduit ni second scheduler, ni Queue, ni daemon, ni sous-système de
ressources.
### Audit des 14 kinds de production
Tous les kinds passent par l'unique Queue et l'unique Governor, y compris ceux
dont toutes les dimensions sont fixes. Dans le tableau, `CPU 1..N` décrit la
réduction possible par l'admission; `lot 1..N` décrit la dimension de lot
adaptable. La mémoire réservée vaut `fixe + par_item * batch_size`. Tous les
coûts GPU par item valent zéro; la forme ORB Vulkan normale réserve exactement
un slot inflight de 640 Kio, débité une fois de la RAM sur UMA. La capacité
privée de sûreté/benchmark peut retenir deux slots, soit 1,25 Mio, seulement
sous un contrat forcé depth 2. Device, pipeline, layouts et cache restent partagés; leurs
allocations driver opaques ne reçoivent pas un coût inventé.
| Kind v1 | Estimation courante | Dimensions réellement consommées / constat Phase 1 |
| --- | --- | --- |
| `raw.develop` | MIXED; CPU 1; lot 1; hôte `2 Gio + contexte`, 0/item; I/O 1; GPU 0 | Fixe et atomique. Un garde Queue applique CPU1 au pool OpenCV process-wide puis restaure la valeur précédente. |
| `photo_quality.triage` | IMPORT; CPU 1; lot 1; hôte `contexte retenu + 20 Mio`, 0/item; I/O 1; GPU 0 | Un groupe par séquence. Un garde Queue applique/restaure CPU1 ; lot et contexte sont honnêtes. |
| `acquisition_campaign.run` | IMPORT; CPU 1; lot 1; hôte `contexte retenu + 256 Kio` + 64 Kio/item; I/O 1; GPU 0 | Sources, confirmations, requête et plan sont chargés avant admission. La reprise remplace seulement l'enveloppe opérationnelle historique sous-estimée par ce coût exact ; le snapshot durable reste inchangé. |
| `import.images` | IMPORT; CPU 1; lot 1..32; hôte 128 Kio + `NAME_MAX+64`/item; I/O 1; GPU 0 | Le callback consomme exactement le lot admis et réadmet entre lots. |
| `features.extract` | CPU; CPU 1..12; lot 1; hôte 64 Mio + 512 Mio/item; I/O 1; GPU 0 | Une image. Le callback applique/restaure exactement le CPU admis dans OpenCV, y compris si la vérification échoue après mutation. Sortie égale à 1/2/4/8/12. |
| `features.extract.sift` | CPU; CPU 1..12; lot 1; hôte 64 Mio + 1 Gio/item; I/O 1; GPU 0 | Même enforcement/rollback adaptatif. La forme durable courante reste CPU12 et la forme historique CPU1 exacte est normalisée en mémoire. |
| `features.extract.rootsift` | CPU; CPU 1..12; lot 1; hôte 64 Mio + 1 Gio/item; I/O 1; GPU 0 | Même exécution adaptative que SIFT ; aucune couture GPU validée. |
| `visual_index.update` | CPU; CPU 1..12; lot 1..16; hôte 8 Mio + 2 Mio/item; I/O 1; GPU 0 | CPU et lot sont lus du contrat; au plus `cpu_threads-1` enfants joints avant publication. |
| `candidate_pair.generate` | CPU; CPU 1..12; lot 1..64; hôte 256 Kio + 64 Kio/item; I/O 1; GPU 0 | CPU et lot sont consommés; fenêtre `min(2*CPU, 24)`. La forme sérielle historique exacte est normalisée en mémoire. |
| `matcher.run` | CPU: CPU 1..12, lot 1..12, hôte 0 + 10 Mio/item, I/O 1, GPU 0. ORB Vulkan normal: CPU 1, lot 1..8, même hôte/item, I/O 1, GPU 1 + 640 Kio, inflight 1. | ORB AUTO adapte seulement le lot sur huit observations pures par palier; CPU complet en fallback. Le benchmark privé conserve batch 12 et depth 2 (1,25 Mio), pas la politique normale. Vulkan explicite et contrôle synchrone restent depth 1; SIFT/RootSIFT et CPU explicite restent fixes. Helpers=0. |
| `geometric_verifier.run` | CPU; CPU 1; lot 1..8; hôte 4 Mio, 0/item; I/O 1; GPU 0 | Le lot est consommé séquentiellement; USAC conserve `isParallel=false`. |
| `track_builder.run` | CPU; CPU 1; lot 1; fixe `(4 Mio + arêtes * (48 + 2*160)) * facteur`, facteur 2 jusqu'à 400k arêtes puis 8; 0/item; I/O 1; GPU 0 | Rebuild atomique. La reprise valide le scope mais ne recalcule pas l'estimation avant admission. |
| `sparse_sfm.run` | CPU; CPU 1; lot 1; fixe `ceil_Mio(128 Mio + 64 Kio/image + 2 Kio/track + 512 octets/observation)`; 0/item; I/O 1; GPU 0 | Exécution atomique; BA à un thread. La forme est redérivée à la reprise sans réconciliation de l'estimation persistée. |
| `incremental_reconstruction.run` | CPU; CPU 1; lot 1; fixe `ceil_Mio(256 Mio + 128 Kio*(caméras base + images extension) + 4 Kio*(landmarks base + tracks extension) + 1 Kio*(observations base + extension))`; 0/item; I/O 1; GPU 0 | Exécution atomique; la forme est redérivée à la reprise sans réconciliation de l'estimation persistée. |
Ces écarts sont des constats d'implémentation Compute Governor v2. Ils ne
créent ni nouvelle limite scientifique, ni nouvelle identité, ni modification
du schéma Project DB. Une enveloppe privée de capacités peut corriger la
sélection/réconciliation sans modifier l'ABI C public du Task Kind Registry.
## SIFT v1A
Une extraction SIFT demande jusqu'à douze threads CPU, un slot IO, aucun GPU,
pour une image. Le Governor réduit ce plafond au budget hôte, identique à la
limite OpenCV process-wide configurée avant les workers. L'estimation
structurelle conservatrice est environ 1,06 Gio (décodage,
pyramides, candidats et F32×128), lot 1, pic de record batch zéro. La
réservation couvre ainsi le fan-out interne sans créer un second pool runtime.
pour une image. L'estimation structurelle conservatrice est environ 1,06 Gio
(décodage, pyramides, candidats et F32×128), lot 1, pic de record batch zéro.
Le callback applique exactement le contrat admis dans `1..12` au pool OpenCV
process-wide sous l'unique propriétaire Queue, puis restaure la valeur
précédente sur toutes les sorties. Les tests déterministes ORB, SIFT et
RootSIFT couvrent 1/2/4/8/12 et obtiennent les mêmes keypoints, descripteurs et
métriques. Cette dimension opérationnelle ne change ni fingerprint ni Feature
Set.
## Responsabilité
@ -19,7 +309,9 @@ Le profil interactif par défaut conserve un quart de la RAM détectée et un
quart des threads logiques pour le système hôte. Sur 16 Gio/16 threads, cela
donne environ 3,8 Gio et 4 threads de headroom. Une nouvelle admission attend
également lorsque PSI CPU `some avg10` atteint 20 %, ou PSI mémoire 1 %. Ces
signaux n'interrompent jamais le petit job déjà réservé.
signaux n'interrompent jamais le petit job déjà réservé. Ces valeurs décrivent
le profil Gate G core gelé; la cible opérationnelle v2 active est le couple
3 GiB/2 GiB et les signaux différentiels définis en tête de document.
La soft floor vaut un quart et la hard floor un huitième de la RAM détectée. La
soft floor place le Governor au minimum en YELLOW ; la hard floor le place
@ -55,8 +347,9 @@ une fois du budget RAM. Le multi-GPU est différé.
Le Governor ne garantit aucune allocation et ne transforme ni swap, ni zram,
ni stockage externe en RAM. Il ne modifie aucun paramètre scientifique. Aucun
scratch, cache de télémétrie, suivi RSS, redimensionnement de réservation ou
monitoring live n'appartient à Gate G core.
scratch, historique RSS long terme, redimensionnement de réservation depuis le
RSS ou monitoring live n'appartient à Gate G core. L'observation courante
bornée RSS/HWM de Compute Governor v2 reste strictement diagnostique.
## API principale
@ -91,6 +384,8 @@ monitoring live n'appartient à Gate G core.
4. Les réservations sont libérées exactement une fois
5. Les estimations de ressources sont immuables
6. L'historique des métriques est strictement borné (8 entrées par classe)
7. Un contrat de séquence est immutable jusqu'à sa libération; seule la
séquence suivante peut être adaptée
## Cycle de vie
@ -118,7 +413,8 @@ monitoring live n'appartient à Gate G core.
## Réserves
- Sous-estimation temporaire possible avec des estimations statiques
- Pas d'adaptation basée sur le débit (duration_ns non encore utilisé)
- L'adaptation de débit v2 reste bornée au dernier état par kind/backend ; elle
ne persiste ni historique volumineux ni décision matérielle.
- L'import `import.images` est admis avec 128 Kio fixes, un coût borné par item,
un thread CPU, un slot I/O et des lots de 1 à 32. Il enregistre le nombre
d'images logiques nouvellement enregistrées dans le ScanSet et la durée
@ -132,7 +428,9 @@ monitoring live n'appartient à Gate G core.
un slot I/O, avec
64 Mio fixes et 512 Mio par image. Cette estimation conservatrice couvre le
chemin actuel sans prétendre mesurer les allocations internes d'OpenCV.
`record_batch` couvre la validation source, le décodage, ORB, la publication
l'admission choisit 1..`min(12, compute_pool)` et le callback applique ce
nombre immutable au pool OpenCV. `record_batch` couvre la validation source,
le décodage, ORB, la publication
et la finalisation DB ; `peak_memory_bytes == 0` signifie « mesure inconnue ».
- `visual_index.update` demande jusqu'à douze threads CPU, un slot I/O, 8 Mio
fixes et 2 Mio par Feature Set, par lots de 1 à 16. Le GPU vaut zéro. Le
@ -155,35 +453,82 @@ monitoring live n'appartient à Gate G core.
et 24 sources au total ; le propriétaire de Task persiste ensuite seul et en
ordre canonique. Cette estimation opérationnelle ne limite pas la taille
scientifique du dataset.
- `matcher.run` demande jusqu'à huit threads CPU, un slot IO et 10 Mio par
- `matcher.run` demande jusqu'à douze threads CPU, un slot IO et 10 Mio par
Candidate Pair admise, correspondant au working set contrôlé inférieur à
environ 10 Mio par paire au
maximum SIFT/RootSIFT (8 Mio de descripteurs contigus, KNN `k=2`, sorties et
fichier bornés), hors scratch interne OpenCV. Ses lots sont bornés à 1, 2, 4
ou 8 Candidate Pairs. **IMPLEMENTATION IN PROGRESS — P4 :** une fenêtre
contient au plus deux paires par thread CPU effectivement admis et huit
fichier bornés), hors scratch interne OpenCV. Ses lots CPU restent bornés à
1..12 Candidate Pairs; ORB Vulkan AUTO normal est borné à 1..8.
**PASS / FROZEN — P4 / Governor v2 :** une fenêtre
contient au plus deux paires par thread CPU effectivement admis et douze
paires au total. Le callback Queue est un participant, crée au plus
`cpu_threads - 1` enfants et les joint avant publication et libération de la
réservation. Chaque paire conserve ses buffers dans un stage privé jusqu'à
sa publication ordonnée ou son nettoyage ; OpenCV reste à un thread interne
pour éviter une sursouscription imbriquée. Le Governor réserve donc au plus
80 Mio contrôlés pour un lot de huit ; cette borne opérationnelle ne limite
120 Mio contrôlés pour un lot de douze ; cette borne opérationnelle ne limite
pas la cardinalité scientifique du dataset.
Le mode d'exécution est fixé par l'estimation immutable avant admission. Le
mode parallèle par défaut est CPU-only et ne réserve aucun GPU, même si le
Governor réduit ensuite son admission à un thread. Un mode explicitement
sériel ORB, avec profil GPU et backend runtime disponibles, demande exactement
un thread CPU, un slot GPU et 640 Kio ; sur UMA ces 640 Kio sont aussi débités
du budget RAM. Sa reconstruction conserve ce mode à partir de l'estimation
durable, sans sélection tardive d'une ressource non réservée.
La reprise accepte les formes courantes exactes CPU8/GPU0 et
CPU1/GPU1/640 Kio ainsi que leurs prédécesseurs exacts CPU12. Elle normalise
éphémèrement CPU12 vers la forme courante du même mode avant admission. Toute
forme voisine est rejetée ; cette compatibilité opérationnelle ne persiste ni
ne déduit une identité backend dans Project DB.
Une Task ORB normale nouvelle persiste la classe honnête `MIXED` avec les
champs opérationnels CPU12/GPU0, lot 1..12 et 10 Mio par paire : cette classe
signifie que la politique AUTO peut exécuter une séquence CPU ou Vulkan, pas
qu'elle réserve les deux simultanément et pas un tag de backend. L'override
CPU explicite persiste la même forme de ressources en classe `CPU`.
La signature durable ORB Vulkan courante demande CPU1/GPU1, lot 1..12 et
640 Kio; son maximum 12 reste une sûreté durable/benchmark, pas le plafond
utile AUTO. L'enveloppe AUTO normale borne le lot à 8 et fige inflight 1; sur
UMA ce payload est
débité une seule fois du budget RAM. Le backend est créé sans payload mappé,
initialise et conserve exactement 640 Kio pour depth 1. La couture privée de
sûreté/benchmark peut porter 1,25 Mio pendant un contrat forcé depth 2. Il ne
redimensionne jamais avec une
requête pending; la fin de séquence libère le second slot avant la prochaine
admission depth 1. Une croissance échouée restaure la capacité antérieure,
et `backend_info` rapporte le payload réellement retenu, non le maximum de
l'enveloppe. Les anciennes
formes CPU8/GPU0 et CPU1/GPU1 à lot maximal 8, puis les formes plus anciennes
CPU12 à 10 Mio fixes, sont seulement des signatures exactes de reprise. Elles
sont normalisées éphémèrement vers la forme courante correspondante; toute
forme voisine est rejetée.
La production normale ORB reconstruit désormais une enveloppe privée
CPU/Vulkan et demande au Governor un choix `AUTO` avant chaque séquence. Une
fois admis, ce choix ne varie jamais dans la séquence. La nouvelle signature
`MIXED` reconstruit AUTO, y compris dans un build portable où son enveloppe
n'expose que CPU. Pour la compatibilité et la sûreté des overrides, toutes les
signatures ORB historiques/courantes de classe `CPU` reconstruisent un CPU
fixe ; une signature Vulkan reste fixe Vulkan. Aucun champ de ressource ne
sert de faux tag et aucun backend ou matériel n'entre dans l'identité
scientifique ou le payload Project DB.
La création et la reconstruction AUTO exposent une capacité Vulkan depuis
les seules métadonnées build/backend/GPU, sans appeler le driver ni
pré-dimensionner la RAM. Le Governor évalue ensuite l'enveloppe exacte contre
son snapshot MemAvailable/PSI/swap et sa charge UMA. Le premier begin et
donc toute initialisation se déroulent sur le worker Queue après application
de son affinité. La politique de cache Mesa est déjà établie au démarrage,
avant ce worker : aucun balayage post-init, latch de Task ou appel d'affinité
par TID auxiliaire n'existe. Une paire localement inéligible n'initialise
toujours pas le backend. Le diagnostic de séquence conserve séparément le
backend
sélectionné et le backend réel (`CPU`, `ORB_VULKAN` ou mix de paires
complètes), avec une raison de fallback. Les participants Matcher CPU sont
comptés uniquement dans `cpu_threads`; `helpers` reste zéro.
La soumission asynchrone est une couture privée de
`src/orb_vulkan_backend_internal.h`, absente de l'ABI public. Le backend
conserve deux slots maximum et un handle exact `slot+generation` par requête;
`finish` ne peut consommer que ce handle et les comptes soumis. Une génération
arrivée à `UINT64_MAX` ne boucle jamais : le slot est retiré avant toute
nouvelle soumission, de sorte qu'un ancien handle ne peut redevenir courant.
Toute
sortie/capacité invalide libère son slot, et
un échec d'attente de fence détruit/met en échec la session avant toute
nouvelle soumission. Les temporaires et objets pending sont nettoyés sur
erreur, exception C++, annulation et échec de publication. Une trace de test
bornée prouve sans temporisation, à depth 2, `SUBMIT(i) < SUBMIT(i+1) <
FINISH(i) < PUBLICATION_START(i) < PUBLICATION_FINISH(i)` pour deux paires
769×769, sans modifier leur sortie.
- `track_builder.run` réserve un worker CPU, aucun GPU et aucun fan-out GVR.
L'estimation est `4 MiB + raw_inlier_edges * (48 + 2*160)` avec facteur 2
sous 400000 arêtes et facteur 8 au-delà, après vérification d'overflow. Le
jusqu'à 400000 arêtes inclusivement et facteur 8 au-delà, après vérification
d'overflow. Le
facteur élevé protège la transition mémoire observée à grande échelle ; le
Governor reste l'unique propriétaire de l'admission et de la pression.
@ -192,6 +537,12 @@ monitoring live n'appartient à Gate G core.
- Worker unique (pas de pools multiples)
- Pas de priorités entre tâches
- Pas de persistance des métriques
- Inflight Vulkan normal est fixé à 1, helpers GPU reste 0 et le lot AUTO
maximal utile est 8. Batch 12 et depth 2 demeurent des capacités privées de
sûreté/benchmark, chacune rejetée comme politique normale faute du gain de
débit durable de 5 %. La publication canonique reste owner-only et représente
environ 29,5 s dans les cohortes contrôlées; avec depth 2 sous 5 %, aucune
preuve ne justifie un helper supplémentaire face à cette frontière durable.
- Pas de communication avec d'autres gouverneurs
Les corrections dérivables G-D01 (`UINT64_MAX` est le dernier ID valide et la

View file

@ -41,6 +41,72 @@ Une panne pré-terminale peut donc répéter la normalisation exacte. Cette cout
ne peut modifier ni identité, paramètres scientifiques, progression ou curseur
métier, et toute forme voisine est rejetée.
## Inventaire production et entrées runtime
`src/task_kinds.c::lardon3d_task_kind_registry_production()` enregistre les
14 kinds v1 du profil de production courant. La colonne « reprise » nomme le
reconstructeur du binding ; « callback » nomme l'entrée runtime privée dans le
même fichier. Le détail chiffré des capacités est centralisé dans l'[audit des
14 kinds](resource_governor.md#audit-des-14-kinds-de-production).
| Kind v1 | Source, reprise et callback | Réconciliation pré-admission courante |
| --- | --- | --- |
| `raw.develop` | `raw_development_task.cpp`; `lardon3d_raw_development_task_reconstruct`; `run` | Aucune |
| `photo_quality.triage` | `photo_quality_task.cpp`; `lardon3d_photo_quality_task_reconstruct`; `run` | Aucune |
| `acquisition_campaign.run` | `acquisition_campaign_task.cpp`; `lardon3d_acquisition_campaign_task_reconstruct`; `run` | Aucune |
| `import.images` | `import_task.c`; `lardon3d_image_import_reconstruct`; `run_image_import` | Aucune |
| `features.extract` | `feature_task.c`; `lardon3d_feature_extract_reconstruct`; `run` | Durable CPU12; admission/runtime OpenCV 1..12 |
| `features.extract.sift` | `sift_task.c`; `lardon3d_sift_extract_reconstruct`; `run` | CPU1 historique exact → durable CPU12; runtime 1..12 |
| `features.extract.rootsift` | `sift_task.c`; `lardon3d_sift_extract_reconstruct`; `run` | CPU1 historique exact → durable CPU12; runtime 1..12 |
| `visual_index.update` | `visual_index_task.c`; `lardon3d_visual_index_update_reconstruct`; `run` | CPU runtime 1..12 ; feedback par segment durable |
| `candidate_pair.generate` | `candidate_pair_task.c`; `lardon3d_candidate_pair_generate_reconstruct`; `run` | Forme sérielle historique exacte → CPU12 ; runtime CPU 1..12 et lot 1..64 |
| `matcher.run` | `matcher_task.c`; `lardon3d_matcher_task_reconstruct`; `run` | Signatures CPU/Vulkan historiques exactes → formes courantes en mémoire |
| `geometric_verifier.run` | `geometric_verifier_task.c`; `lardon3d_geometric_verifier_task_reconstruct`; `run` | Aucune |
| `track_builder.run` | `track_builder_task.cpp`; `lardon3d_track_builder_task_reconstruct`; `run` | Aucune |
| `sparse_sfm.run` | `sparse_sfm_task.cpp`; `lardon3d_sparse_sfm_task_reconstruct`; `run` | Aucune |
| `incremental_reconstruction.run` | `incremental_reconstruction_task.cpp`; `lardon3d_incremental_reconstruction_task_reconstruct`; `run` | Aucune |
## Couture privée Compute Governor v2
**COMPUTE_GOVERNOR_V2 — PASS / FROZEN.** Le descriptor C public
reste limité à kind, version et reconstructeur. L'enveloppe de capacités est
intégrée sans changement d'ABI dans le `struct Lardon3DTask` opaque et les
coutures privées `src/task_internal.h` / `src/resource_governor_internal.h`.
Les coutures d'admission sont `src/task_queue.c::select_admissible()`,
`src/task.c::lardon3d_task_sequence_break()` et, côté Governor,
la sélection multi-capacité sur un snapshot unique. La normalisation historique
exacte reste dans `src/task_kind_registry.c::normalize_known_legacy_estimate()`.
Cette enveloppe n'est ni une identité scientifique, ni un nouveau payload
Project DB, ni un nouveau scheduler. Le Governor possède l'admission de tous
les kinds, y compris les formes entièrement fixes. Le contrat choisi est
immutable pendant une séquence et seule la suivante peut être adaptée. Une Task
sans alternative reçoit automatiquement une capacité égale à son estimation
durable. Le Governor conserve un état borné par kind/backend et un dernier
diagnostic ; ni l'enveloppe ni ce choix ne sont persistés.
La politique CPU hôte reste privée au Governor : masque permis, groupes
package/core/SMT, compute-pool et résultat d'application du worker Queue. Le
compute-pool borne l'admission de chaque kind, y compris une capacité durable
CPU12. Feature/SIFT/RootSIFT consomment le compte immutable 1..12 dans OpenCV ;
les kinds CPU1 restent fixes. Aucun ID CPU ou choix d'affinité n'entre dans le
descriptor, le checkpoint ou le Project DB.
Le feedback ne requalifie pas un succès de reprise en travail durable : les
kinds Feature/SIFT/RootSIFT comptent un item seulement après extraction et
publication durable propre. READY, collision `ALREADY_PRESENT` ou publication
incertaine compte zéro ; Visual Index compte pareillement zéro pour un segment
`PUBLISHED_NOT_DURABLE`.
L'état privé par kind/backend coordonne désormais une seule dimension d'essai.
Les CPU validés slow-startent `1/2/4/8/12`; après deux observations de baseline,
deux observations à au moins +5 % sont nécessaires pour accepter le palier.
Une fois CPU stabilisé, seuls les kinds dont le callback consomme réellement
son lot peuvent ouvrir un essai de lot. `features.extract`, SIFT et RootSIFT
enregistrent une observation atomique réussie partagée entre Tasks ; Visual
Index, Candidate et Matcher enregistrent chaque séquence. Les autres formes ou
dimensions non adaptables restent égales à leur capacité fixe honnête.
## Persistance et legacy
Le checkpoint générique reste en version 1. Project Database v7 conserve le
@ -78,21 +144,95 @@ demande d'un thread CPU par douze avant admission. Le checkpoint historique et
le curseur typé restent inchangés. Les snapshots Candidate courants et tous les
autres kinds restent inchangés.
**IMPLEMENTED** — `matcher.run`, version 1, recharge la configuration Matcher,
l'identité Feature Set et le curseur `after_candidate_pair_id`. Il traite une
Candidate Pair atomique à la fois dans des lots bornés à huit, checkpoint le
curseur et repasse par le Governor entre les lots. La table durable
`matcher_tasks` est introduite par Project DB v11, après le Match Result v10.
Son reconstructeur accepte les formes opérationnelles exactes CPU8/GPU0 et
CPU1/GPU1/640 Kio, puis normalise éphémèrement leurs deux prédécesseurs CPU12
vers la forme courante correspondante avant l'admission. Une forme
voisine échoue au lieu de servir d'indice de backend ; le payload Project DB ne
change pas et ne persiste aucune identité matérielle.
**PASS / FROZEN — Compute Governor v2.** `matcher.run`, version 1,
recharge la configuration Matcher, l'identité Feature Set et le curseur
`after_candidate_pair_id`. Il traite une Candidate Pair atomique à la fois dans
des lots bornés à douze, checkpoint le curseur et repasse par le Governor entre
les lots. La table durable `matcher_tasks` est introduite par Project DB v11,
après le Match Result v10. Son reconstructeur accepte les formes courantes
CPU12/GPU0 et CPU1/GPU1/640 Kio à lot `1..12`, les signatures historiques
CPU8/GPU0 et Vulkan à lot maximal 8, puis les formes CPU12
pré-estimation-par-paire. La normalisation reste en mémoire. Une forme voisine
échoue au lieu de servir d'indice de backend ; le payload Project DB ne change
pas et ne persiste aucune identité matérielle. Les nouvelles Tasks ORB normales
ont une signature de classe `MIXED`, dont les autres champs restent une demande
de ressources réelle ; elle seule reconstruit la politique Governor `AUTO`.
Toutes les formes ORB de classe `CPU`, anciennes ou courantes, reconstruisent
un CPU fixe pour préserver les overrides explicites et une compatibilité sûre ;
une forme Vulkan restaurée reste fixe Vulkan. Un build portable reconstruit la
même politique `MIXED` mais n'expose que sa capacité CPU. Les snapshots tout à
zéro Candidate/SIFT/RootSIFT sont explicitement corrompus ; seules leurs
signatures historiques complètes exactes sont acceptées. Seule la forme AUTO
restaurée établit la disponibilité Vulkan partagée. Restaurer ensuite CPU,
Vulkan ou une signature historique fixe n'écrit rien dans cet état : la
co-restauration est indépendante de l'ordre. Aucun nouvel état de
backend n'est persisté.
Pour une Task AUTO, la Registry reconstruit aussi l'enveloppe privée Vulkan
CPU1/GPU1, lot opérationnel `1..8`, helpers 0 et inflight 1. La signature
durable historique reste à lot `1..12`; la signature 640 Kio
reste la forme depth-1 minimale et n'est pas mutée; l'admission normale facture
exactement 640 Kio une seule fois sur UMA. Le choix inflight est immutable dans
la séquence et ne devient ni payload, ni fingerprint, ni indice de reprise.
Vulkan explicite reste depth 1. La capacité privée de sûreté/benchmark peut
forcer deux slots et 1,25 Mio sans changer la reconstruction normale.
Le backend ne mappe pas le maximum de l'enveloppe à sa création : il retient
exactement un slot à depth 1 et deux seulement sous une séquence depth 2 admise,
puis libère le second avant l'admission suivante. La signature durable 640 Kio
reste donc inchangée sans sous-facturer une allocation depth 2 forcée.
L'A/B forcé ABBA a mesuré 54,661652238 paires/s à depth 1 et 55,797311953 à
depth 2, soit +2,077617 %, sous le deadband 5 %, avec digest identique, quatre
séquences de fallback local par exécution et zéro panne/discard. La Registry conserve donc
`DEPTH_MAX_VALIDATED_SAFETY=2` pour les seules coutures privées, mais la
capacité AUTO normale suit `DEPTH_MAX_USEFUL=1` : depth 2 est
**REJECTED_WITH_MEASURED_REASON**, sans nouvelle signature durable.
La télémétrie privée de `matcher.run` conserve les classes de fallback par
séquence et compte aussi les items exacts local-ineligible/backend-failure/other
après leur publication durable. Ce détail opérationnel n'ajoute aucun kind,
champ durable ou identité et empêche le regroupement batch de devenir un
comparateur scientifique. Le commit immédiat par item reste acquis si une
paire suivante avorte, tandis que le feedback de séquence n'est pas enregistré;
la déduplication actuelle vit seulement avec la Task reconstruite en mémoire.
Les logs batch 2/4 antérieurs à ce compteur restent préliminaires et prouvent
seulement l'invalidité du comparateur par séquences. Les huit runs item-valides
`forced-batch{2,4,8,12}-items{,-b}.stdout.jsonl` conservent chacun 4113 paires,
six items locaux, zéro panne/autre et le même digest. Les débits combinés sont
54,180767704, 66,094373197, 74,784998723 et 76,755814095 paires/s. Les gains
jusqu'à batch 8 dépassent 5 %, celui de 8 à 12 vaut seulement +2,635308425 % :
la Registry expose `BATCH_MAX_USEFUL=8` en AUTO normal et réserve batch 12 aux
preuves privées (`REJECTED_WITH_MEASURED_REASON`).
Le S21 final confirme l'enveloppe Registry en production : `matcher.run` v1
reste le même kind durable, AUTO choisit Vulkan pour 21 630 admissions et
termine 172 741/172 741 résultats à batch 8/inflight 1/helpers 0. Le passage
transitoire 8 → 1 → 2 → 4 → 8 ne modifie ni signature durable, ni
fingerprint, ni digest scientifique. Aucun backend ou champ persistant n'est
ajouté par cette adaptation.
La reconstruction AUTO ne sonde ni n'initialise Vulkan sur le thread
`project_open()`. Elle expose la capacité depuis les seules métadonnées runtime
build/backend/GPU ; le Governor possède le dimensionnement exact et
l'admission UMA sur son snapshot. Le premier begin appartient au worker Queue après son
affinité. La politique Mesa sûre est déjà établie avant les pthreads et
l'initialisation du driver ; aucun sweep/latch auxiliaire n'appartient donc au
contexte Task. Une paire localement inéligible n'initialise pas le backend. Une
panne réelle produit des paires CPU complètes et rend le backend indisponible
aux admissions AUTO suivantes sans réécrire le snapshot durable.
Le contrôle de benchmark `synchronous` du runner réel n'étend pas le descriptor
ni le reconstructeur. Il est compilé hors du binaire production, attaché
seulement au contexte éphémère d'une nouvelle Task et refusé par le runner si
une Task Matcher doit être reprise. La Registry continue donc à reconstruire
uniquement la politique AUTO/fixe déduite de la signature durable, jamais un
pipeline de benchmark.
**IMPLEMENTED** — `features.extract.sift` et `features.extract.rootsift`
acceptent leur forme CPU12 courante et normalisent uniquement leur forme CPU1
historique exacte. Cette compatibilité opérationnelle n'altère ni fingerprint,
Feature Set, checkpoint durable, ni politique scientifique.
historique exacte. ORB/SIFT/RootSIFT appliquent ensuite le compte CPU admis dans
`1..12`; les sorties testées à 1/2/4/8/12 restent égales. Cette compatibilité
opérationnelle n'altère ni fingerprint, Feature Set, checkpoint durable, ni
politique scientifique.
**IMPLEMENTED** — `geometric_verifier.run`, version 1, recharge la configuration
Fundamental immuable, en revalide le fingerprint et reprend `after_match_result_id`.

View file

@ -87,8 +87,11 @@ affecter les autres entrées de l'inventaire.
## Invariants
1. **Estimation immuable** : une fois créée, l'estimation d'une tâche ne change
jamais. Elle est copiée en lecture seule lors de la réservation.
1. **Estimation durable immuable** : l'estimation stockée dans la Task courante
ne change jamais. Compute Governor v2 utilise une enveloppe privée de
capacités, distincte de ce snapshot et non persistée comme identité. Le
contrat choisi pour une séquence est lui aussi immutable jusqu'à sa
libération ; seule une séquence suivante peut être adaptée.
2. **Transitions d'état validées** : le cycle nominal est
`PENDING → RUNNING → COMPLETED/FAILED/CANCELLED`, avec pause coopérative.
3. **Pause et annulation coopératives** : le callback appelle périodiquement
@ -105,7 +108,147 @@ affecter les autres entrées de l'inventaire.
- **task_queue** : la file gère l'ordre d'exécution et invoque les callbacks.
- **resource_governor** : l'estimation est utilisée pour la réservation avant
exécution.
- **scheduler** : le scheduler transmet l'estimation lors de la soumission.
- **scheduler** : ses responsabilités restent représentées par le runtime et
l'unique Queue existants ; aucun second scheduler n'est introduit.
### Frontière Compute Governor v2
**COMPUTE_GOVERNOR_V2 — PASS / FROZEN.** Chaque kind de production
reste propriétaire d'une Task et passe par l'unique Queue à un worker, puis par
l'unique Resource Governor, même lorsque CPU, lot, mémoire, I/O et GPU sont
tous fixes. Une dimension fixe n'autorise jamais à contourner l'admission.
Le code conserve une `Lardon3DResourceEstimate` canonique immutable pour la
durabilité. Derrière les types opaques, chaque Task possède désormais une
enveloppe privée bornée : par défaut une capacité fixe exactement égale à cette
estimation ; seuls les kinds possédant des alternatives réelles en ajoutent.
Queue et `sequence_break()` demandent au Governor de choisir et réserver une
capacité depuis un seul snapshot courant. L'exécution reçoit ce contrat
immutable et ne le renégocie pas en cours de séquence. Cette couture n'étend ni
le descriptor public du Task Kind Registry, ni le checkpoint, ni le payload
Project DB et n'ajoute aucune dimension d'identité scientifique.
Le Governor possède aussi la politique CPU hôte privée. Depuis le masque
permis et la topologie package/core, il dérive un compute-pool par coeurs
physiques complets ; un caller déjà précontraint fournit directement son pool.
Sans topologie exploitable, le budget portable subsiste avec affinité inactive.
Seul le worker lourd de Queue applique et vérifie son propre masque (`pid=0`)
avant les callbacks, donc le creator/main/TUI reste libre. Le Governor ne mute
jamais un TID auxiliaire énuméré : un `PIDFD_THREAD` ne stabilise pas le numéro
TID consommé par `sched_setaffinity(tid)`. Le démarrage établit plutôt
`MESA_SHADER_CACHE_DISABLE=true` avant tout pthread applicatif et toute
initialisation Vulkan. Une absence prend ce défaut sûr ; `true`/`1` explicites
sont conservés, tandis qu'une valeur explicite fausse ou malformée est respectée
mais entraîne un refus de démarrage. Cette politique opérationnelle, sans état
Task durable ni identité scientifique, supprime les helpers de cache Mesa
observés qui élargissaient leur masque. Il n'existe plus de sweep post-init,
latch de Task ou retry de recontrainte auxiliaire ; les diagnostics indiquent
l'état et la raison de la politique sans prétendre une activité auxiliaire.
Le nombre du pool borne toute admission CPU. Cette couture
n'ajoute ni scheduler, ni worker, ni champ durable ou ABI publique.
Le comportement de production normal ORB est `AUTO` Governor-owned. Les modes
CPU/Vulkan explicites sont réservés au debug, benchmark et à la
reproductibilité. Une Task normale nouvelle persiste une classe `MIXED`
sémantiquement honnête ; cette signature reconstruit AUTO, tandis que toute
signature CPU ORB ancienne ou courante reconstruit un CPU fixe et que Vulkan
reste fixe. Création et reprise n'initialisent pas Vulkan sur le caller ; le
premier begin appartient au worker Queue contraint. Le choix matériel ne
devient ni payload ni identité. Seule une reprise AUTO établit la disponibilité
Vulkan partagée ; les reprises fixes et historiques sont sans effet global,
donc leur ordre ne dégrade pas AUTO. Une panne backend est publiée avant tout
fallback ou sortie précoce ; une inéligibilité de paire reste locale et ne
possède aucun handle à terminer. Le Governor conserve une télémétrie fixe et
bornée par kind/backend (dernière durée, travail durable, débit, backend
sélectionné/réel, contrat, pression et raison) et aucune grande histoire
persistante. L'adaptation générique utilise deux observations de référence et
deux du palier d'essai. Le lot ORB Vulkan, plus bruité, exige huit séquences
pures consécutives pour chacune de ces fenêtres avant toute décision ; une
observation Matcher mesure la cadence de bout en bout depuis la réadmission
réussie jusqu'au checkpoint générique durable, et non le seul callback de
calcul. Le temps de calcul seul reste diagnostique. Ainsi le Governor apprend
le coût de séquence amorti par le lot et un checkpoint échoué ne l'entraîne pas.
Une
admission adaptative encore permise sous pression installe
CPU1, lot minimum et inflight minimum avant sa réservation. Inflight ORB normal
reste fixé à 1; helpers reste 0. Les callbacks atomiques n'annoncent
un item que si extraction et publication propre sont durables ; READY,
`ALREADY_PRESENT` et `PUBLISHED_NOT_DURABLE` sont des observations zéro qui
n'avancent pas la rampe.
L'A/B forcé ABBA a mesuré seulement +2,077617 % à depth 2
(54,661652238 contre 55,797311953 paires/s), sous le deadband 5 %, avec digest identique,
quatre séquences de fallback local par exécution et zéro panne/discard. Depth 2 est donc
**REJECTED_WITH_MEASURED_REASON** pour AUTO normal. Compute Governor v2 reste
en cours jusqu'aux réconciliations restantes.
Pour Matcher Vulkan, le lease privé de capacité matérialise le contrat de la
séquence seulement après son admission. Il alloue un ou deux payloads de
640 Kio sans requête pending, les conserve jusqu'au nettoyage complet de la
séquence, puis rend depth 2 à depth 1 avant `sequence_break()`. Une allocation
depth 2 échouée ne modifie pas le contrat scientifique et conduit à la paire CPU
complète; elle ne laisse ni réservation suivante sous-facturée, ni résultat
Vulkan partiel.
Cette boucle est maintenant `observe → choose → execute → measure → adapt next`.
Les CPU réductibles progressent uniquement par `1/2/4/8/12`, bornés par le
compute-pool ; CPU et lot ne sont jamais essayés ensemble. Les observations
hôte privées comprennent utilisation du pool, mémoire/PSI/swap actif, GPU busy
et RSS observé, sans confondre RSS et réservation. Le dernier diagnostic peut
être tiré par numéro de série ou formaté dans un buffer borné ; le runtime ne
l'imprime pas directement dans le TUI. Le contrat déjà installé demeure
immutable même si une nouvelle observation arrive pendant son exécution.
Un agrégat privé de taille fixe complète ce dernier diagnostic : il compte les
admissions et séquences effectivement enregistrées et somme leurs métriques en
saturant, afin qu'un poller lent ne transforme pas des changements coalescés en
fausse histoire exhaustive. Il vit uniquement avec le Governor courant.
Pour la matrice forcée du runner, cet agrégat sépare les fallbacks par
inéligibilité locale, panne backend et raison autre/inconnue, à la fois par
séquence diagnostique et par item exact. Le compte d'items avance une seule fois
après la publication durable du fallback CPU complet; le travail d'une
séquence sélectionnée CPU reste à zéro. Ce commit opérationnel immédiat survit à
l'échec ou l'annulation d'une paire suivante, mais ne crée ni séquence réussie,
ni débit durable, ni adaptation. Un high-water mark non persisté le rend
idempotent pendant la vie de la Task. La Task n'expose alors qu'une capacité
Vulkan aux batch/depth demandés : absence de GPU/backend/mémoire ne peut donc
pas devenir une admission CPU. Une panne backend tardive laisse la publication
CPU complète déjà durable mais fait échouer la Task de benchmark après
checkpoint; seul le compte d'items localement inéligibles, égal entre cohortes
comparées même lorsque leur batch diffère, reste admissible pour l'évidence.
Les anciens logs batch 2/4, à quatre contre trois séquences locales pour les
mêmes items/digest, sont préliminaires et ne déterminent aucun débit utile. La
matrice item-valide suivante mesure batch 2/4/8/12 à
54,180767704/66,094373197/74,784998723/76,755814095 paires/s. Les deux premiers
gains de palier dépassent le deadband, celui de 8 à 12 vaut seulement
+2,635308425 % : AUTO normal s'arrête à batch 8 et batch 12 reste un contrôle
privé sûr rejeté pour la politique normale.
Le run sans override `short-auto-batch8-governor-v2.stdout.jsonl` valide ensuite
la Task réelle : `1 → 2 → 4 → 8`, 4113/4113 résultats, 76,072 paires/s, digest
identique, six items locaux et aucune panne/discard. La cadence de séquence
inclut réadmission et checkpoint durable; inflight reste 1 et helpers 0.
Le run S21 final crée ensuite la Task normale 2831 en mode AUTO, sans option
backend/lot/inflight. Ses 21 630 admissions sont toutes Vulkan; la Task publie
172 741/172 741 résultats, termine `COMPLETE` à 100 %, avec zéro doublon et
curseur complet. La seule admission YELLOW produit un contrat batch 1, puis
les admissions GREEN suivantes remontent de façon bornée jusqu'à batch 8. Le
contrat actif n'est jamais muté sous le callback : chaque changement appartient
à la séquence suivante.
Le runner d'évidence réel crée les nouvelles Tasks Matcher normales par l'API
AUTO. Son contrôle `synchronous` est un flag de contexte compilé seulement dans
le target benchmark/test : il ne modifie ni Task durable, ni envelope, ni
contrat installé, ni callback de production. Puisque ce flag n'est pas
checkpointé, le runner refuse expressément de l'appliquer à une reprise Matcher
pendante. Le chemin rolling/recovery normal reste inchangé.
Dans ce même target seulement, `--matcher-inflight 1|2` reconstruit avant la
création d'une Task neuve une enveloppe AUTO Vulkan à profondeur fixe et batch
2 par défaut. `--matcher-batch 2|4|8|12`, valable seulement avec inflight et
rolling AUTO, fixe aussi le lot. Ces contrôles ne remplacent pas le Governor : l'admission, la mémoire
par slot, l'UMA et la réservation de séquence restent identiques. Le contrôle
est refusé pour les modes explicites, pour synchronous depth 2 et pour une Task
pendante. Une garde de processus restaure les tokens privés sur toute sortie;
aucun token, champ de contexte, payload ou comportement correspondant n'est
compilé dans `lardon3d`.
## Statut
@ -134,11 +277,25 @@ candidates avec idempotence, checkpoint après chaque lot et repasse par le
Governor via `sequence_break`. La reprise est idempotente avec le curseur
`after_feature_set_id` rechargé depuis la DB.
**IMPLEMENTED** — `matcher.run` traite une Candidate Pair atomique à la fois,
par lots adaptatifs de 1, 2, 4 ou 8. Il persiste le curseur
**PASS / FROZEN — Compute Governor v2.** `matcher.run` traite une
Candidate Pair atomique à la fois, par lots opérationnels bornés jusqu'à 12.
Le code courant consomme honnêtement CPU, lot et GPU du contrat choisi. Feature,
SIFT et RootSIFT appliquent l'admission OpenCV `1..12`; RAW et Photo Quality
restent CPU1. Pour ORB normal, le Governor choisit GPU-first ou CPU complet pour la prochaine
séquence, sans mutation pendant son exécution. Ces dimensions ne changent ni
identité scientifique ni publication. Le callback persiste le curseur
`after_candidate_pair_id`, checkpoint après publication de chaque lot et
effectue une rupture de séquence avant le suivant. Une paire repassée après un
crash est réutilisée par son Match Result.
crash est réutilisée par son Match Result. La soumission Vulkan rolling est
privée et request-bound; le contrat normal de séquence fige inflight 1. Deux slots
maximum restent disponibles à la couture privée de sûreté/benchmark et portent
chacun command/fence/buffers/query et un handle exact tandis
que device/pipeline/layout/cache restent partagés. Le propriétaire soumet
jusqu'à la profondeur admise, finit le plus ancien et publie toujours le
préfixe canonique contigu; toute sortie, annulation ou exception nettoie les
handles encore privés. Les buffers du second slot ne sont mappés que pendant
une séquence depth 2 admise et sont libérés avant la rupture suivante. Helpers
reste 0 et le contrôle synchrone force depth 1.
**IMPLEMENTED** — `geometric_verifier.run` v1 traite un Match Result atomique à
la fois, par lots adaptatifs de 1, 2, 4 ou 8. Project DB v13 conserve sa

View file

@ -33,6 +33,18 @@ device, la file, le pipeline, le command buffer et trois buffers
bornés. Un mutex impose un dispatch à la fois. Une famille compute sans graphics
est préférée, avec fallback vers toute famille compute compatible.
Le propriétaire du processus doit établir `MESA_SHADER_CACHE_DISABLE` à la
valeur exacte `true` ou `1` avant de créer ses threads. Les exécutables
Lardon3D normaux prennent le défaut sûr si la variable est absente et refusent
une valeur explicite différente. Le backend public peut cependant être appelé
par un autre consumer après son propre démarrage : sa frontière d'initialisation
ne mute donc jamais l'environnement. Avant tout appel Vulkan/Mesa, elle refuse
une valeur absente, fausse ou malformée, mémorise le backend comme indisponible
et retourne `LARDON3D_ORB_VULKAN_UNAVAILABLE` sans sortie partielle. Les appels
vides et la lecture metadata restent non initialisants. Les exécutables autonomes
de benchmark/feasibility établissent le même défaut sûr comme première action de
`main` ; cette politique reste opérationnelle, non persistée et non scientifique.
Le sélecteur utilise le travail `feature_count_a × feature_count_b`. Sous le
seuil mesuré de `768 × 768` comparaisons, OpenCV reste utilisé afin d'éviter le
coût fixe du dispatch. Le seuil est une politique d'exécution et ne modifie ni
@ -41,8 +53,15 @@ matche aucun grand couple ORB ne paie aucun cold start.
## Mémoire et pannes
Les buffers maximaux contiennent 256 Kio pour A, 256 Kio pour B et 128 Kio pour
8192 sorties top-2, soit 640 Kio de payload, hors petits objets du driver. Une
Chaque slot contient 256 Kio pour A, 256 Kio pour B et 128 Kio pour 8192
sorties top-2, soit 640 Kio. Le contexte fraîchement créé ne mappe aucun
payload. L'initialisation depth 1, rolling AUTO normal et le wrapper public
synchrone retiennent exactement un slot. Seul un contrat privé de
sûreté/benchmark depth 2 mappe le second, soit 1,25 Mio pendant cette séquence.
Device, pipeline,
layouts et cache restent partagés, hors objets opaques du driver. Commandes,
fences, descriptor sets et queries sont des métadonnées bornées à deux slots,
mais ne rendent pas le second payload mappé tant qu'il n'est pas admis. Une
mémoire host-visible, cohérente et cached est préférée sur UMA, car elle réduit
nettement le coût CPU de copie/readback observé sur RADV. Le backend sait
appliquer flush/invalidate lorsque le type retenu n'est pas cohérent.
@ -52,12 +71,44 @@ utilise le CPU sans nouvelle tentative par paire. Une panne de soumission ou un
device lost désactive Vulkan pour la session ; la paire courante est reprise sur
CPU avant toute publication. Un Match Result n'est jamais créé depuis une
sortie GPU partielle.
La couche Matcher distingue cette faute backend d'une faute locale. Une lecture
Feature/allocation échouée avant `begin` ne consomme aucun slot; une faute de
filtrage/allocation/staging après un `finish` backend réussi a déjà consommé
seulement son handle exact. Ces deux cas recalculent la paire complète sur CPU,
comptent `other` après publication durable et conservent la disponibilité du
backend ainsi que tout successeur soumis sain.
Le job est synchrone au niveau du Matcher. Il soumet sur l'unique queue détenue
par le contexte puis attend cette queue, sans `vkDeviceWaitIdle` sur le chemin
normal. Le Resource Governor admet la tâche avant le callback ; en RED aucun
nouveau batch ne démarre, tandis qu'une paire déjà soumise finit et se publie.
Le worker unique et le mutex interdisent plusieurs dispatchs concurrents en v1.
Le Resource Governor fige inflight 1 avant le callback AUTO normal. La couture
benchmark/test peut forcer 1 ou 2. Au début de la séquence, sans requête pending,
le backend alloue la capacité exacte avant le
premier submit ; une croissance échouée conserve la capacité antérieure et
provoque le fallback CPU complet. Le worker unique soumet jusqu'à cette
profondeur sur l'unique queue, attend chaque fence exacte et publie le préfixe
en ordre, sans `vkDeviceWaitIdle` sur le chemin normal. La fin de séquence,
succès, annulation ou erreur nettoyée, libère le second payload avant la
prochaine admission depth 1. En RED aucun nouveau batch ne démarre; la pression
active ramène l'admission courante permise au depth minimum. Le wrapper public
et le contrôle de benchmark synchrone forcent depth 1. Aucun helper hôte n'est
créé.
L'ABBA corpus forcé mesure 54,661652238 paires/s à depth 1 et 55,797311953 à
depth 2, soit +2,077617 %, sous le deadband 5 %, avec digest identique, quatre fallbacks
locaux et zéro panne/discard. Depth 2 est donc
**REJECTED_WITH_MEASURED_REASON** pour AUTO normal; sa capacité maximale 2
reste disponible uniquement pour les preuves privées reproductibles.
Le S21 AUTO final exerce le backend normal sur 172 741 paires : 172 507
soumissions et 172 507 complétions, zéro panne, zéro discard et aucun slot
pending à la sortie. Les 234 items sous le seuil restent des fallbacks locaux
CPU complets; ils ne sont jamais passés à `finish`. Le digest canonique final
est `e5128a2e599ff593c4f79850e067254b1f249d19e8480a44973306b1af250f70`
pour 172 741 mappings contigus et sans doublon. La réservation AUTO reste un
slot de 655 360 octets UMA pendant tout ce run.
Chaque requête privée porte le couple exact `slot+generation`. La génération
ne boucle jamais : après l'usage de `UINT64_MAX`, ce slot est retiré avant toute
nouvelle soumission et ne peut plus faire correspondre un handle ancien. Un
second slot sain peut terminer sa requête indépendamment. `finish` et `discard`
consomment uniquement le handle exact et ne mélangent jamais leurs sorties.
## Shader et déterminisme

View file

@ -16,17 +16,48 @@ chemins SIMD jusqu'à AVX512-SKX. Il a été compilé avec OpenCL, mais
`AMD Radeon 780M Graphics (RADV PHOENIX)`, API 1.4.354, avec une file compute
dédiée et de la mémoire UMA host-visible/cohérente.
Le runtime actuel de Lardon3D possède un worker. Le profil interactif conserve
12 threads OpenCV process-wide, quatre threads logiques pour le desktop et un
seul Matcher actif. Lorsque
les pools multi-workers seront introduits, la cible de départ recommandée est
deux Matchers avec huit threads OpenCV chacun pour une charge mixte. Les mesures
montrent toutefois que quatre Matchers à quatre threads favorisent SIFT et les
cas 4096, tandis que deux à huit favorisent ORB 8192. Le Governor devra donc
choisir à partir de la classe de charge, pas d'une constante universelle. Le
nombre de threads OpenCV devra être réglé une fois au démarrage :
`cv::setNumThreads()` est une configuration globale et ne doit jamais être
modifiée concurremment par des workers.
Le sysfs amdgpu de cet hôte expose 512 Mio dans `mem_info_vram_total` et
7 986 020 352 octets dans `mem_info_gtt_total`. Le profil matériel conserve les
512 Mio comme capacité de payload observable, mais la combinaison petit
aperture/GTT à l'échelle de la RAM suffit à classer ce GPU shared/UMA sans
hardcoder son device ID. Le Governor débite alors les ressources GPU exactement
une fois de `MemAvailable` et n'utilise jamais cet aperture comme mémoire libre
séparée pour contourner les objectifs 3 Gio/2 Gio.
Le runtime actuel de Lardon3D possède un worker lourd. Le profil interactif
établit au démarrage une baseline/plafond OpenCV de 12 threads et réserve quatre
threads logiques au desktop. Pour chaque séquence, l'unique callback Queue
applique temporairement le compte CPU immuable admis dans `1..12`, le vérifie et
restaure la baseline sur toute sortie. `cv::setNumThreads()` reste une
configuration process-wide : sa mutation concurrente par plusieurs workers
n'est pas supportée. Un futur pool multi-worker devrait donc changer ce modèle
explicitement, pas multiplier silencieusement ces mutations globales.
Le worker Queue applique à lui-même le compute-pool `0-5,8-13`, tandis que le
creator/main reste unrestricted `0-15`. Certains helpers de cache disque Mesa
observés avaient réélargi leur affinité après l'initialisation lazy. Comme un
pidfd ne stabilise pas le TID numérique pour `sched_setaffinity(tid)`, Lardon3D
ne tente aucune mutation auxiliaire. Il établit plutôt
`MESA_SHADER_CACHE_DISABLE=true` avant toute création de pthread applicatif et
toute initialisation Vulkan. Une absence prend ce défaut sûr ; les valeurs
explicites exactes `true`/`1` sont respectées, tandis qu'une valeur explicite
fausse ou malformée reste inchangée mais interdit le démarrage. Sur la 780M
validée, les helpers `*:disk$0` sont absents et tous les threads runtime vivants
observés gardent `0-5,8-13`. Cette politique est opérationnelle et inoffensive
hors Mesa ; elle n'ajoute ni identité scientifique, ni état durable, ni sweep
post-init.
Le backend public ne suppose pas que tout consumer traverse ce démarrage : sa
première requête Vulkan non vide vérifie `true`/`1` sans modifier
l'environnement, avant tout appel Mesa. Une valeur absente ou différente
retourne `UNAVAILABLE`, mémorise l'indisponibilité et ne produit aucune sortie.
Les benchmarks et la feasibility autonomes établissent le défaut sûr comme
première action de `main` ; les lectures metadata restent non initialisantes.
Les anciennes mesures exploratoires montrent que quatre Matchers à quatre
threads favorisent SIFT et les cas 4096, tandis que deux à huit favorisent ORB
8192. Elles n'établissent pas un pool de production actuel. Tout futur modèle
devrait être choisi par le Governor à partir de la classe de charge et prouver
un contrôle de concurrence compatible avec la configuration globale OpenCV.
Quatre Matchers avec un ou deux threads chacun dégradent fortement les grands
cas ORB. Quatre fois quatre threads augmente le working set et la variance, mais
@ -65,10 +96,24 @@ environ 0,10, 0,96, 14,7 et 59,6 ms pour BFMatcher CPU lors de la campagne
production. L'initialisation lazy mesurée vaut environ 129136 ms. Le seuil
`feature_count_a × feature_count_b >= 768²` évite le GPU pour les petits travaux.
La mémoire permanente directement contrôlée vaut 640 Kio de payload. Les tests
de parité couvrent exactement le top-2 jusqu'à 8192, le Match File complet et le
fallback CPU. Ces nombres décrivent la machine mesurée et ne sont pas un contrat
portable de latence.
La mémoire directement contrôlée vaut 640 Kio par slot; rolling AUTO peut
réserver un ou deux slots, soit au plus 1,25 Mio, débités une fois de la RAM sur
la 780M UMA. Le backend mappe exactement cette capacité pendant la séquence et
rend le second slot avant une admission depth 1 suivante. Les tests de parité
couvrent exactement le top-2 jusqu'à 8192, le Match File complet et le fallback
CPU. Ces nombres décrivent la machine mesurée et ne sont pas un contrat portable
de latence.
Le run S21 AUTO final sur cette cible mesure 172 741 résultats durables en
2 345,444485079 s, soit 73,649/s. Sur les échantillons connus, GPU busy vaut
26 % en moyenne, 27 % en médiane et 36 % au maximum; l'utilisation moyenne
connue du compute-pool vaut 9,68 % de ses 12 CPU logiques. Le RSS observé
culmine à 168 980 480 octets et le HWM à 250 658 816 octets. Le minimum
`MemAvailable` reste 10 927 390 720 octets, PSI mémoire maximal et deltas swap
restent nuls. Après le run, Sway répond, Firefox et son flux PipeWire vers le
casque actif restent présents; PSI mémoire avg10/60/300 est nul. Ces signaux
objectifs attestent la conservation du desktop, sans prétendre mesurer une
perception subjective.
## Geometric Verifier Fundamental — Gate A

View file

@ -232,11 +232,224 @@ GPU soit approprié ni qu'une sortie GPU partage automatiquement l'identité
scientifique CPU. Le gel ne revendique pas de comparaison de débit durable
CPU-versus-GPU sur corpus : la pression hôte a contaminé cette mesure.
Un éventuel **GPU COMPUTE v1** est postérieur et optionnel. Il ne peut être
ouvert que si un audit futur et une preuve d'équivalence à la frontière concernée
le justifient ; sinon le chemin CPU demeure le chemin retenu. Il ne modifie pas
les contrats scientifiques, les résultats canoniques ni l'unique gouvernance des
ressources.
Cette limite de preuve v1 n'annule pas l'évidence directe fournie à la tranche
suivante. Pour le hot path ORB Matcher, Vulkan est désormais validé,
déterministe et mesurément supérieur ; il devient donc le workload primaire de
la politique v2 GPU-first, sous admission GPU/UMA, avec fallback CPU. Candidate,
Feature et Visual Index restent explicitement rejetés pour le GPU, et
SIFT/RootSIFT Matcher restent BFMatcher L2 CPU.
**COMPUTE_GOVERNOR_V2 — PASS / FROZEN.**
**ORB_VULKAN_ASYNC_EXECUTION — PASS / FROZEN.** Cette tranche
autorisée fait évoluer l'unique Governor, sans créer de scheduler, Queue,
daemon ou persistance parallèle : télémétrie bornée, admission adaptative par
séquence et `AUTO` GPU-first pour les backends exacts et mesurément supérieurs.
La couture ORB Matcher normale est gelée avec CPU complet en fallback ; les
choix CPU/Vulkan explicites restent des overrides de debug, benchmark et
reproductibilité. Les dimensions retenues et toutes les validations v2 sont
closes.
CPU12 est validé. Le Governor dérive désormais le pool lourd depuis le masque
permis et les groupes package/core/SMT. Sur l'hôte unrestricted courant, il
obtient `0-5,8-13` et réserve `6,7,14,15`; un caller déjà précontraint ne subit
pas une seconde réserve. Le worker Queue seul applique/vérifie son propre
masque ; aucun TID auxiliaire énuméré n'est muté, car un pidfd ne stabilise pas
le numéro consommé par `sched_setaffinity(tid)`. Avant tout pthread applicatif
ou driver, le démarrage établit `MESA_SHADER_CACHE_DISABLE=true`; une valeur
absente prend ce défaut, `true`/`1` explicites sont conservées et toute autre
valeur explicite est préservée mais refusée. Cette politique non scientifique
supprime les helpers de cache Mesa observés qui élargissaient leur masque. Les
threads runtime restants héritent le compute-pool ; il n'existe plus de sweep,
latch ou retry auxiliaire et le diagnostic expose la politique réelle.
Creator/main/TUI reste unrestricted. Le fallback au budget portable ne crée
aucune exclusion
inventée. Cette couture est **PASS / FROZEN** sur le profil validé; ces IDs ne
sont pas une politique portable. La cible RAM conserve
3 GiB de `MemAvailable` et ne franchit pas intentionnellement le plancher dur de
2 GiB. Les PSI CPU/mémoire/I/O et les deltas swap-in/swap-out sont des signaux
actifs ; l'occupation totale du swap reste historique. Admission CPU et lot
sont indépendantes. Sur la 780M, Hardware Profile classe conservativement comme
UMA le petit aperture VRAM amdgpu de 512 Mio accompagné d'environ 7,99 Go de
GTT système ; la capacité rapportée reste observable mais ne devient pas un
budget séparé. Les coûts GPU sont débités exactement une fois de la RAM hôte.
L'audit Phase 1 couvre les 14 kinds de production et sépare leurs dimensions
fixes des dimensions réellement adaptables. Il confirme que tous passent par
l'unique Governor, même les formes fixes, et que le contrat reste immutable
pendant une séquence. Cette tranche ferme l'enveloppe privée, la sélection AUTO,
les diagnostics bornés, l'enforcement OpenCV adaptatif 1..12, la politique
d'affinité privée et la réconciliation du contexte retenu des campagnes
nouvelles ou restaurées. La création/reprise AUTO ne touche plus Vulkan sur le
main ; le premier begin appartient au worker contraint. Inflight ORB normal est
maintenant fixé à 1, helpers reste 0; depth 2 reste une capacité privée de
sûreté/benchmark. La clôture v2 est acquise. Ces choix ne créent aucune limite
de dataset, identité scientifique ou version Project DB.
La signature durable des nouvelles Tasks ORB normales est maintenant la classe
`MIXED`, sémantiquement réelle pour une politique susceptible d'exécuter CPU ou
Vulkan. Elle reconstruit AUTO ; toutes les signatures CPU anciennes/courantes
restent CPU fixes et Vulkan reste fixe, sans migration DB/codec. La couture
asynchrone est privée, request-bound et nettoie son slot sur toute sortie. Une
preuve événementielle bornée établit la soumission du successeur avant la
publication du prédécesseur pour deux paires 769×769. La rampe ne croît plus sur
la seule santé : l'adaptation générique exige deux observations par fenêtre,
tandis que le lot ORB Vulkan en exige huit, avec retour au palier accepté sans
gain et reset immédiat sous pression. Ces
éléments sont **PASS / FROZEN** dans les limites validées.
Les diagnostics de séquence distinguent maintenant backend sélectionné et
backend réel ; les participants CPU Matcher restent `cpu_threads` et
`helpers=0`. Une paire Vulkan inéligible ou en panne est recalculée entièrement
sur CPU sans preuve partielle. Les extractions ORB/SIFT/RootSIFT testées à
1/2/4/8/12 consomment leur contrat OpenCV immutable avec sorties égales.
Le rolling distingue désormais un handle soumis d'une inéligibilité locale et
n'appelle jamais `finish` sans requête. La panne backend invalide immédiatement
l'admission partagée sur toute sortie précoce ; seules les reprises AUTO peuvent
établir cette disponibilité, indépendamment de l'ordre des reprises fixes ou
historiques. Le statut est **PASS / FROZEN**.
La boucle privée mesure désormais l'utilisation `/proc/stat` du compute-pool,
`MemAvailable`, PSI mémoire/I/O `some/full`, deltas swap actifs, RSS/HWM observé
et GPU busy DRM, avec `unknown` sur absence ou parse non strict. Le backend
Vulkan fournit des compteurs cumulatifs bornés de submit/complétion/fence/
readback/GPU/starvation/panne/discard ; Matcher agrège en plus CPU et publication
par séquence. Le diagnostic est tirable par numéro de série, sans log ncurses ni
histoire persistée. Les CPU réductibles progressent `1/2/4/8/12` après deux
observations de baseline et deux gains d'au moins 5 % ; CPU et lot ne changent
jamais dans le même essai. Sous pression, une admission adaptative encore
permise réserve immédiatement CPU1 et lot minimum. Feature/SIFT/RootSIFT ne
comptent un item qu'après extraction et publication durable propre ; READY,
`ALREADY_PRESENT` et publication incertaine comptent zéro, comme un segment
Visual Index non durable.
Candidate mesure chaque séquence. Un
fallback réel annule l'essai Vulkan au lieu d'empoisonner sa baseline. Cette
implémentation est **PASS / FROZEN** sans ajouter de helper GPU.
L'évidence retenue avant cette implémentation compare le contrôle synchrone à
49,989 paires/s et rolling depth 1 à 55,124 paires/s (+10,27 %), avec le même
digest `7a9dbc38a23a600379167d55e24836b7acbb22eea25573e7440bdc9e4602b3b3`.
La starvation depth 1 (53,847 s sur 74,613 s, GPU busy max 25 %) motive deux
slots bornés mais ne constitue pas une mesure depth 2. Le backend partage
device/pipeline/layout/cache et duplique seulement 640 Kio de payload,
command/fence/descriptors/query par slot. Le payload mappé suit désormais
exactement la capacité de séquence admise : zéro avant initialisation, 640 Kio
à depth 1 et 1,25 Mio uniquement pendant un contrôle privé depth 2, avec retour
à 640 Kio avant la prochaine admission depth 1. L'enveloppe normale n'essaie
plus inflight 2. Les générations de requête ne bouclent pas; un slot épuisé est
retiré définitivement.
Le harness réel a exécuté le corpus 4113 paires pour le contrôle synchrone,
rolling depth 1 et l'A/B forcé depth 1/depth 2. L'ABBA forcé donne
54,661652238 et 55,797311953 paires/s (+2,077617 %, sous le deadband 5 %).
Chaque run porte 4113 paires durables; le débit combiné 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 %. Les quatre exécutions conservent le
digest `7a9dbc38a23a600379167d55e24836b7acbb22eea25573e7440bdc9e4602b3b3`, quatre
séquences de fallback local par exécution et zéro panne/discard. Depth 2 est donc
**REJECTED_WITH_MEASURED_REASON** pour la politique normale :
`DEPTH_MAX_VALIDATED_SAFETY=2`, `DEPTH_MAX_USEFUL=1`. Une comparaison
whole-corpus adaptative antérieure (+1,12 %) ne séparait pas les changements de
contrat; l'ABBA ci-dessus la remplace comme décision de profondeur. Le
répertoire retenu est
`/home/fy59/Documents/Lardon/.real-pre-sfm-2026-08-30/governor-v2-evidence/`,
avec `forced-depth1-a.stdout.jsonl`, `forced-depth1-b.stdout.jsonl`,
`forced-depth2-a.stdout.jsonl` et `forced-depth2-b.stdout.jsonl`. Le
mode Matcher par défaut est AUTO/rolling ; CPU et Vulkan explicites restent des
overrides. Un contrôle synchrone fence par fence est compilé seulement dans le
runner/test, absent de `lardon3d`, non persisté et non applicable à une reprise
Matcher pendante. Les fixtures et ces deux exécutions établissent l'égalité
exacte des sorties; les chiffres depth 1 sont rapportés ci-dessus. Le runner émet
des diagnostics JSON échantillonnés sans prétendre voir chaque séquence, plus
un agrégat Governor fixe exact pour les compteurs enregistrés. Il valide la
bijection Candidate Pair/Match Result, les assets et le curseur, puis produit le
digest canonique de comparaison `L3DMRD1`, qui exclut IDs Task, timestamps et
choix opérationnels. Ces preuves ferment **PASS / FROZEN** sans promouvoir
depth 2 dans AUTO normal.
Le runner possède maintenant, dans ce seul target, `--matcher-inflight 1|2` :
AUTO rolling fixe batch 2 et min=max inflight pour permettre la mesure du même
binaire à profondeur 1 puis 2, en n'exposant que la capacité Vulkan forcée.
`--matcher-batch 2|4|8|12`, valable seulement avec inflight et AUTO rolling,
fixe aussi min=max batch pour la prochaine matrice contrôlée.
Synchronous reste depth 1. Le Governor conserve son admission et sa charge UMA;
GPU budget zéro ou capacité GPU/backend/mémoire indisponible échoue au lieu de
sélectionner CPU. La valeur n'est ni persistée ni scientifique,
est refusée sur une Task pendante, restaurée dans l'environnement à toute sortie
et émise comme `1`, `2` ou `null` dans les résumés/agrégats. Le runner valide le
contrat exact et zéro panne/discard/pending; seule l'inéligibilité locale peut
produire un fallback CPU complet. La production compte désormais chaque item
exactement une fois après publication durable dans local-ineligible,
backend-failure ou other; une admission CPU normale reste à zéro. Le compteur
est incrémenté immédiatement et reste visible si une paire suivante échoue ou est
annulée, sans valider ni entraîner la séquence incomplète; son high-water mark
de déduplication reste privé et non persisté. L'égalité
inter-cohortes porte sur les items locaux, pas sur les séquences dont le nombre
dépend du batch. Une panne/raison autre, ou une panne tardive, invalide la
cohorte; la Task de benchmark échoue après checkpoint de toute preuve CPU déjà
durable. Les fixtures
établissent l'égalité exacte rolling1/rolling2/synchrone1 et des batches
2/4/8/12 sur fixture, avec le même compte de 29 items locaux malgré des comptes
de séquences différents. Les logs corpus préliminaires
`forced-batch2-current.stdout.jsonl` et `forced-batch4.stdout.jsonl` couvrent
les mêmes 4113 IDs/digest mais portent quatre contre trois séquences locales :
ils sont conservés comme preuve que l'ancien comparateur était invalide et ne
participent pas à la décision. Les huit runs item-valides
`forced-batch{2,4,8,12}-items{,-b}.stdout.jsonl` publient chacun 4113 paires,
six items locaux, zéro panne/autre et le même digest. Leurs débits combinés
sont 54,180767704, 66,094373197, 74,784998723 et 76,755814095 paires/s. Batch
4 puis 8 gagnent +21,988624373 % et +13,148812987 %; batch 12 ne gagne que
+2,635308425 %, sous le deadband 5 %. AUTO normal suit donc
`BATCH_MAX_USEFUL=8`; batch 12 reste une capacité privée sûre
`REJECTED_WITH_MEASURED_REASON`.
Le contrôle de production sans override
`short-auto-batch8-governor-v2.stdout.jsonl` atteint réellement
`1 → 2 → 4 → 8`, puis publie 4113/4113 résultats à 76,072 paires/s avec le
même digest, six fallbacks locaux, zéro panne/discard, inflight 1 et helpers 0.
La preuve de fermeture S21
`final-s21-auto.stdout.jsonl` exécute ensuite le chemin production normal sur
172 741 Candidate Pairs : 172 741 Match Results, zéro doublon, curseur complet,
digest `e5128a2e599ff593c4f79850e067254b1f249d19e8480a44973306b1af250f70`
et 73,649 résultats durables/s. AUTO choisit Vulkan sur toutes les admissions,
termine batch 8/inflight 1/helpers 0 et ne compte aucune panne/discard/pending.
Une admission YELLOW réduit batch 8 à 1, puis les séquences GREEN rétablissent
1 → 2 → 4 → 8; le gate possède donc aussi une preuve réelle de recovery.
Les gates de fermeture sont acquis :
```text
GOVERNOR_CONTROLS_ALL_TASKS=PASS
HOST_CPU_RESERVE=PASS
HOST_RAM_RESERVE=PASS
REAL_TIME_TELEMETRY=PASS
SLOW_START=PASS
HYSTERESIS=PASS
PRESSURE_THROTTLE=PASS
PRESSURE_RECOVERY=PASS
GPU_FIRST_AUTO=PASS
ORB_VULKAN_TRUE_ASYNC=PASS
TRUE_GPU_CPU_OVERLAP=PASS
VULKAN_RESOURCE_BOUNDS=PASS
UMA_ACCOUNTING=PASS
SCIENTIFIC_EQUIVALENCE=PASS
RESTART_IDEMPOTENCE=PASS
FINAL_FULL_S21_AUTO_RUN=PASS
FULL_NORMAL_SUITE=PASS
C17=PASS
ASAN_UBSAN=PASS
GIT_DIFF_CHECK=PASS
FINAL_XHIGH_REVIEW=PASS
DOC_CONSISTENCY=PASS
MATCHER_GPU=EXISTING_BACKEND_VALIDATED_AND_PREFERRED
COMPUTE_GOVERNOR_V2=PASS/FROZEN
ORB_VULKAN_ASYNC_EXECUTION=PASS/FROZEN
```
L'expérience normale est donc : l'utilisateur lance une Task; l'unique
Governor choisit et explique le contrat borné de sa prochaine séquence. Aucun
réglage CPU/GPU/lot/inflight/helper n'est requis en production ordinaire.
L'ordre de travail autorisé à court terme est :
@ -245,7 +458,7 @@ fondation scientifique pré-SfM courante
→ INTERNAL PARALLELISM + COMPUTE RESOURCES v1
(Candidate, Matcher, Visual Index, audit feature threading,
correction progression/Resource Governor, audit GPU)
GPU COMPUTE v1 optionnel, seulement si audit et équivalence le justifient
COMPUTE GOVERNOR v2 / ORB VULKAN ASYNC EXECUTION (PASS / FROZEN)
→ poursuite pré-SfM réelle complète de S21
→ acquisition dédiée de calibration
→ Sparse SfM réel
@ -561,7 +774,8 @@ L'ordre demeure sans ambiguïté :
CURRENT NEXT
fondation scientifique pré-SfM courante
→ INTERNAL PARALLELISM + COMPUTE RESOURCES v1
→ GPU COMPUTE v1 optionnel si audit/équivalence le justifient
→ COMPUTE GOVERNOR v2 / ORB VULKAN ASYNC EXECUTION
(PASS / FROZEN; AUTO GPU-first ORB et preuve S21 Matcher complète)
→ poursuite pré-SfM réelle complète de S21
→ acquisition physique dédiée de calibration
→ calibration connue validée → Sparse SfM réel multi-campagne

View file

@ -13,7 +13,9 @@
enum {
LARDON3D_MATCHER_TASK_KIND_VERSION = 1,
LARDON3D_MATCHER_TASK_MINIMUM_BATCH = 1,
LARDON3D_MATCHER_TASK_MAXIMUM_BATCH = 8,
/* This is an operational window and participant ceiling, not a scientific
* dataset limit. Every staged pair remains private until ordered publish. */
LARDON3D_MATCHER_TASK_MAXIMUM_BATCH = 12,
};
typedef struct {
@ -23,11 +25,12 @@ typedef struct {
Lardon3DMatcherParams matcher;
} Lardon3DMatcherTaskConfiguration;
/* Selects only the admitted execution resources and runtime backend. Matcher
* parameters, fingerprints, Match Results, and Match Files are identical
* across modes. CPU_PARALLEL is portable and is the default used by the
* existing create/enqueue APIs. ORB_VULKAN requires an ORB configuration, a
* selected GPU, and an available Vulkan backend when the task is created. */
/* Selects only an explicit debug/benchmark/reproduction override. Normal
* create/enqueue is Governor-owned AUTO: validated ORB Vulkan is preferred
* when runtime support and admission are safe, otherwise the complete CPU
* implementation is selected. Matcher parameters, fingerprints, Match
* Results, and Match Files are identical across operational selections.
* SIFT/RootSIFT remain CPU-only. */
typedef enum {
LARDON3D_MATCHER_TASK_MODE_CPU_PARALLEL = 0,
LARDON3D_MATCHER_TASK_MODE_ORB_VULKAN = 1,

View file

@ -12,6 +12,11 @@ extern "C" {
typedef struct Lardon3DOrbVulkanBackend Lardon3DOrbVulkanBackend;
enum {
/* Stable minimum/depth-1 payload cost used by durable resource signatures:
* two 8192x32 descriptor buffers plus one 8192x4x32-bit top-2 buffer. The
* private rolling backend may own two such slots; backend_info reports the
* ORB request-slot payload actually retained at the observation instant
* without changing this public ABI. */
LARDON3D_ORB_VULKAN_PERMANENT_BUFFER_BYTES = 640 * 1024,
};
@ -52,24 +57,45 @@ typedef struct {
uint64_t gpu_ns;
} Lardon3DOrbVulkanInfo;
/* Create an uninitialized backend without probing Vulkan or changing process
* environment. The caller owns the returned object and must exclude every
* concurrent or future use before destroy; destroy accepts NULL. */
Lardon3DOrbVulkanBackend *lardon3d_orb_vulkan_backend_create(void);
void lardon3d_orb_vulkan_backend_destroy(Lardon3DOrbVulkanBackend *backend);
bool lardon3d_orb_vulkan_should_use(uint32_t feature_count_a,
uint32_t feature_count_b);
/* Compute deterministic ORB/Hamming top-2 results. Nonzero counts are bounded
* to 8192; output_capacity covers feature_count_a.
* Empty A succeeds without output, and empty B writes zero-neighbor rows.
*
* In a Vulkan-enabled build, the first nonempty request may initialize Mesa.
* Before any process thread is created, the process owner must establish
* MESA_SHADER_CACHE_DISABLE to exact "true" or "1". The backend never mutates
* that environment. An absent, false, or malformed value makes the first
* initializing request return UNAVAILABLE, caches that state for this backend,
* and leaves output untouched. Invalid arguments return INVALID_ARGUMENT;
* initialization absence/failure returns UNAVAILABLE, while an initialized
* request/session failure returns FAILED. */
Lardon3DOrbVulkanResult lardon3d_orb_vulkan_top2(
Lardon3DOrbVulkanBackend *backend, const unsigned char *descriptors_a,
uint32_t feature_count_a, const unsigned char *descriptors_b,
uint32_t feature_count_b, Lardon3DOrbTop2 *output, size_t output_capacity);
#ifdef LARDON3D_SIFT_VULKAN_FEASIBILITY
/* Feasibility-only SIFT top-2 obeys the same bounds, ownership, process-policy,
* UNAVAILABLE caching, and no-partial-output rules as ORB top-2. */
Lardon3DOrbVulkanResult lardon3d_sift_vulkan_top2(
Lardon3DOrbVulkanBackend *backend, const float *descriptors_a,
uint32_t feature_count_a, const float *descriptors_b,
uint32_t feature_count_b, Lardon3DSiftTop2 *output, size_t output_capacity);
#endif
/* Read current backend metadata without probing or initializing Vulkan and
* without requiring or changing MESA_SHADER_CACHE_DISABLE. Both pointers are
* required; the caller owns the output snapshot. A policy-rejected backend is
* initialized=true and available=false. */
bool lardon3d_orb_vulkan_backend_info(Lardon3DOrbVulkanBackend *backend,
Lardon3DOrbVulkanInfo *info);

View file

@ -129,6 +129,7 @@ if matcher_vulkan_enabled
endif
opencv_test_environment = environment()
opencv_test_environment.set('MESA_SHADER_CACHE_DISABLE', 'true')
if get_option('b_sanitize').contains('thread')
opencv_test_environment.set(
'TSAN_OPTIONS',
@ -136,7 +137,7 @@ if get_option('b_sanitize').contains('thread')
)
endif
executable(
lardon3d_app = executable(
'lardon3d',
sources: [
'src/main.c',
@ -221,6 +222,15 @@ executable(
+ matcher_backend_dependencies,
)
# The helper directly execs the production binary and checks its exact early
# rejection; no shell or ncurses session participates in this boundary test.
test(
'app-driver-policy-unsafe',
python,
args: [files('tests/test_driver_policy_startup.py'), lardon3d_app.full_path()],
timeout: 5,
)
import_test = executable(
'test-import',
sources: [
@ -489,10 +499,7 @@ photo_quality_restart_scan = executable(
# Opt-in real-data evidence runner. It is intentionally not registered with
# meson test: invocation requires explicit source roots and a fresh project
# directory, and may perform bounded but long RAW/ORB/matching work.
pre_sfm_real_execution = executable(
'pre-sfm-real-execution',
sources: [
'tests/pre_sfm_real_execution.cpp',
pre_sfm_real_execution_support_sources = [
'src/app_state.c',
'src/acquisition_pairing.cpp', 'src/acquisition_ingest.cpp',
'src/acquisition_campaign.cpp', 'src/acquisition_campaign_task.cpp',
@ -512,19 +519,45 @@ pre_sfm_real_execution = executable(
'src/task.c', 'src/task_checkpoint.c', 'src/task_kind_registry.c', 'src/task_kinds.c',
'src/task_queue.c', 'src/resource_governor.c', 'src/resource_snapshot.c',
'src/hardware_profile.c',
] + matcher_backend_sources,
] + matcher_backend_sources
pre_sfm_real_execution = executable(
'pre-sfm-real-execution',
sources: ['tests/pre_sfm_real_execution.cpp'] +
pre_sfm_real_execution_support_sources,
include_directories: include_directories('include'),
c_args: [
'-DLARDON3D_TRACK_BUILDER_TASK_AVAILABLE',
'-DLARDON3D_ACQUISITION_CAMPAIGN_TASK_AVAILABLE',
'-DLARDON3D_PHOTO_QUALITY_TASK_AVAILABLE',
'-DLARDON3D_RAW_DEVELOPMENT_TASK_AVAILABLE',
'-DLARDON3D_MATCHER_TASK_BENCHMARK_PIPELINE',
],
cpp_args: ['-DLARDON3D_MATCHER_TASK_BENCHMARK_PIPELINE'],
dependencies: [
threads, sqlite3, openssl, opencv, libraw, libexif, libpng, libdeflate,
] + matcher_backend_dependencies,
)
pre_sfm_runner_options_test = executable(
'test-pre-sfm-runner-options',
sources: ['tests/test_pre_sfm_runner_options.cpp'] +
pre_sfm_real_execution_support_sources,
include_directories: include_directories('include'),
c_args: [
'-DLARDON3D_TRACK_BUILDER_TASK_AVAILABLE',
'-DLARDON3D_ACQUISITION_CAMPAIGN_TASK_AVAILABLE',
'-DLARDON3D_PHOTO_QUALITY_TASK_AVAILABLE',
'-DLARDON3D_RAW_DEVELOPMENT_TASK_AVAILABLE',
'-DLARDON3D_MATCHER_TASK_BENCHMARK_PIPELINE',
],
cpp_args: ['-DLARDON3D_MATCHER_TASK_BENCHMARK_PIPELINE'],
dependencies: [
threads, sqlite3, openssl, opencv, libraw, libexif, libpng, libdeflate,
] + matcher_backend_dependencies,
)
test('pre-sfm-runner-options', pre_sfm_runner_options_test, timeout: 30)
feature_store_test = executable(
'test-feature-store',
sources: [
@ -627,9 +660,7 @@ candidate_pair_task_test = executable(
test('candidate-pair-task', candidate_pair_task_test, timeout: 60,
env: opencv_test_environment)
matcher_task_test = executable(
'test-matcher-task',
sources: [
matcher_task_test_sources = [
'tests/test_matcher_task.c', 'src/app_state.c', 'src/project.c',
'src/project_db.c', 'src/project_db_sparse_sfm.c', 'src/task.c', 'src/task_checkpoint.c',
'src/task_kind_registry.c', 'src/task_kinds.c', 'src/task_queue.c',
@ -643,7 +674,11 @@ matcher_task_test = executable(
'src/feature_extractor_opencv.cpp', 'src/visual_index.c',
'src/visual_index_task.c', 'src/candidate_pair_gen.c',
'src/candidate_pair_task.c',
] + matcher_backend_sources,
] + matcher_backend_sources
matcher_task_test = executable(
'test-matcher-task',
sources: matcher_task_test_sources,
c_args: [
'-DLARDON3D_PROJECT_DB_TESTING',
'-DLARDON3D_MATCHER_TASK_TESTING',
@ -653,6 +688,27 @@ matcher_task_test = executable(
)
test('matcher-task', matcher_task_test, timeout: 60, env: opencv_test_environment)
# This separately named evidence test is the only test-side consumer of the
# synchronous-fence control. Ordinary Matcher tests compile the same production
# sources without the macro, matching the production executable boundary.
matcher_task_benchmark_pipeline_test = executable(
'test-matcher-task-benchmark-pipeline',
sources: matcher_task_test_sources,
c_args: [
'-DLARDON3D_PROJECT_DB_TESTING',
'-DLARDON3D_MATCHER_TASK_TESTING',
'-DLARDON3D_MATCHER_TASK_BENCHMARK_PIPELINE',
] + (matcher_vulkan_enabled ? ['-DLARDON3D_MATCHER_TASK_VULKAN'] : []),
include_directories: include_directories('include'),
dependencies: [threads, sqlite3, openssl, opencv] + matcher_backend_dependencies,
)
test(
'matcher-task-benchmark-pipeline',
matcher_task_benchmark_pipeline_test,
timeout: 60,
env: opencv_test_environment,
)
feature_task_test = executable(
'test-feature-task',
sources: [
@ -672,8 +728,11 @@ feature_task_test = executable(
] + matcher_backend_sources,
c_args: [
'-DLARDON3D_PROJECT_DB_TESTING', '-DLARDON3D_FEATURE_TASK_TESTING',
'-DLARDON3D_FEATURE_STORE_TESTING',
'-DLARDON3D_VISUAL_INDEX_TASK_TESTING',
'-DLARDON3D_VISUAL_INDEX_TESTING',
],
cpp_args: ['-DLARDON3D_FEATURE_TASK_TESTING'],
include_directories: include_directories('include'),
dependencies: [threads, sqlite3, openssl, opencv] + matcher_backend_dependencies,
)
@ -737,6 +796,7 @@ task_test = executable(
'src/resource_governor.c',
'src/resource_snapshot.c',
],
c_args: ['-DLARDON3D_TASK_TESTING'],
include_directories: include_directories('include'),
dependencies: [threads],
)
@ -1060,7 +1120,7 @@ matcher_test = executable(
dependencies: [threads, sqlite3, openssl, opencv] + matcher_backend_dependencies,
)
test('matcher', matcher_test, timeout: 60)
test('matcher', matcher_test, timeout: 60, env: opencv_test_environment)
if matcher_vulkan_enabled
orb_vulkan_backend_test = executable(
@ -1130,7 +1190,8 @@ matcher_e2e_test = executable(
dependencies: [threads, sqlite3, openssl, opencv] + matcher_backend_dependencies,
)
test('matcher-e2e', matcher_e2e_test, timeout: 60)
test('matcher-e2e', matcher_e2e_test, timeout: 60,
env: opencv_test_environment)
executable(
'benchmark-matcher',

View file

@ -3,6 +3,7 @@ extern "C" {
#include <lardon3d/project.h>
#include <lardon3d/project_db.h>
#include <lardon3d/task_queue.h>
#include "task_internal.h"
}
#include <cstdio>
@ -184,6 +185,30 @@ struct Context {
Lardon3DAcquisitionIngestOptions options{};
};
bool context_owned_bytes(const Context &context, uint64_t &bytes) {
uint64_t total = sizeof(Context);
const size_t source_count = context.sources.capacity();
const size_t confirmation_count = context.confirmations.capacity();
if (source_count > UINT64_MAX / sizeof(Lardon3DAcquisitionCampaignSource) ||
confirmation_count >
UINT64_MAX / sizeof(Lardon3DAcquisitionCampaignConfirmation))
return false;
const uint64_t retained[] = {
static_cast<uint64_t>(source_count) *
sizeof(Lardon3DAcquisitionCampaignSource),
static_cast<uint64_t>(confirmation_count) *
sizeof(Lardon3DAcquisitionCampaignConfirmation),
static_cast<uint64_t>(context.encoded.capacity()),
};
for (uint64_t amount : retained) {
if (amount > UINT64_MAX - total)
return false;
total += amount;
}
bytes = total;
return true;
}
void destroy(void *p) { delete static_cast<Context *>(p); }
void runtime(Context *c, Lardon3DAppState &s) {
lardon3d_app_state_init(&s);
@ -335,6 +360,32 @@ Context *make_context(const char *path, Lardon3DProjectDb *db,
}
} // namespace
extern "C" bool
lardon3d_acquisition_campaign_task_internal_configure_restored(
Lardon3DTask *task, void *userdata) {
try {
auto *context = static_cast<Context *>(userdata);
uint64_t retained = 0;
if (!task || !context || !context_owned_bytes(*context, retained) ||
retained > UINT64_MAX - 256 * 1024)
return false;
Lardon3DTaskCapability capability{};
capability.estimate = Lardon3DResourceEstimate{
retained + 256 * 1024, 0, 64 * 1024, 0, 1, 1, 1, 0, 1,
LARDON3D_RESOURCE_TASK_IMPORT};
capability.backend = LARDON3D_RESOURCE_BACKEND_FIXED;
capability.inflight_limit = 1;
Lardon3DTaskCapabilityEnvelope envelope{};
envelope.count = 1;
envelope.capabilities[0] = capability;
/* Recovery may carry the historical under-estimate, but admission uses the
* exact newly reconstructed retained context. Durable bytes stay untouched. */
return lardon3d_task_internal_set_capability_envelope(task, &envelope);
} catch (...) {
return false;
}
}
bool request_encode_impl(
const Lardon3DAcquisitionCampaignTaskRequest *r, unsigned char *out,
size_t cap, size_t *size) {
@ -498,8 +549,16 @@ Lardon3DTask *create_task_impl(
delete c;
return nullptr;
}
uint64_t retained = 0;
if (!context_owned_bytes(*c, retained) || retained > UINT64_MAX - 256 * 1024) {
delete c;
return nullptr;
}
/* The request vectors and inline campaign plan remain live for the Task
* lifetime. Charge them before admission; the per-group working estimate is
* separate and this operational bound does not limit scientific cardinality. */
Lardon3DResourceEstimate e{
256 * 1024, 0, 64 * 1024, 0, 1,
retained + 256 * 1024, 0, 64 * 1024, 0, 1,
1, 1, 0, 1, LARDON3D_RESOURCE_TASK_IMPORT};
auto *t = lardon3d_task_create_typed("Campagne d'acquisition", &e,
LARDON3D_ACQUISITION_CAMPAIGN_TASK_KIND,

View file

@ -1,4 +1,5 @@
#include <locale.h>
#include <stdio.h>
#include <stdlib.h>
#include <lardon3d/app.h>
@ -12,9 +13,22 @@
#include <lardon3d/task_queue.h>
#include <lardon3d/tui.h>
#include "resource_governor_internal.h"
int
lardon3d_app_run(void)
{
/* CONTRACT: establish the process-wide driver policy before Queue, OpenCV,
* ncurses, or any other application pthread can exist. An explicit unsafe
* Mesa cache value is rejected instead of silently overwritten. */
Lardon3DResourceDriverPolicyResult driver_policy =
lardon3d_resource_governor_internal_configure_driver_policy();
if (driver_policy == LARDON3D_RESOURCE_DRIVER_POLICY_FAILED
|| driver_policy == LARDON3D_RESOURCE_DRIVER_POLICY_REJECTED_UNSAFE) {
(void)fprintf(stderr,
"MESA_SHADER_CACHE_DISABLE must be true for safe CPU affinity\n");
return EXIT_FAILURE;
}
Lardon3DAppState state;
lardon3d_app_state_init(&state);
@ -39,10 +53,11 @@ lardon3d_app_run(void)
if (feature_threads > 12) {
feature_threads = 12;
}
/* OpenCV owns an internal process-wide pool. Configure it once, before
* Queue workers exist, to the empirically audited part of the host-reserved
* CPU budget. Feature Tasks request the same count from the Governor. This
* operational ceiling is not part of a fingerprint or FeatureSet identity. */
/* OpenCV owns one process-wide CPU setting. Startup establishes the safe
* audited baseline/ceiling before Queue exists; the sole Queue callback may
* temporarily apply its immutable admitted count and must restore this
* baseline on every path. Concurrent multi-worker mutation is unsupported.
* This operational count is never FeatureSet identity or fingerprint. */
if (!lardon3d_feature_opencv_configure_threads(feature_threads)) {
return EXIT_FAILURE;
}

View file

@ -14,6 +14,7 @@
#include <lardon3d/task_queue.h>
#include "candidate_pair_gen_internal.h"
#include "task_internal.h"
enum {
CANDIDATE_PAIR_MINIMUM_BATCH = 1,
@ -339,6 +340,8 @@ static bool run(Lardon3DTask *task, void *userdata) {
(void)lardon3d_resource_governor_record_batch(
context->governor, LARDON3D_RESOURCE_TASK_CPU,
(size_t)seq_generated, elapsed_ns(begin, end), 0);
(void)lardon3d_task_internal_record_sequence(
task, elapsed_ns(begin, end), processed_in_sequence);
Lardon3DAppState state;
runtime_state(context, &state);

View file

@ -20,13 +20,36 @@ extern "C" bool lardon3d_feature_opencv_configure_threads(unsigned int threads)
if (threads == 0 || threads > static_cast<unsigned int>(std::numeric_limits<int>::max())) {
return false;
}
cv::setNumThreads(static_cast<int>(threads));
return cv::getNumThreads() == static_cast<int>(threads);
try {
cv::setNumThreads(static_cast<int>(threads));
#ifdef LARDON3D_FEATURE_TASK_TESTING
const char *forced = std::getenv(
"LARDON3D_TEST_OPENCV_CONFIGURE_FAILURE_THREADS");
if (forced) {
char *end = nullptr;
unsigned long parsed = std::strtoul(forced, &end, 10);
if (end && *end == '\0' && parsed == threads) {
/* Inject after mutation so Task cleanup must restore the captured
* process-wide value rather than treating failure as side-effect-free. */
return false;
}
}
#endif
return cv::getNumThreads() == static_cast<int>(threads);
} catch (...) {
/* This process-wide operational control is a C ABI seam. OpenCV failures
* are reported to the Task owner and no C++ exception may escape. */
return false;
}
}
extern "C" unsigned int lardon3d_feature_opencv_thread_count(void) {
int threads = cv::getNumThreads();
return threads > 0 ? static_cast<unsigned int>(threads) : 1U;
try {
int threads = cv::getNumThreads();
return threads > 0 ? static_cast<unsigned int>(threads) : 1U;
} catch (...) {
return 1U;
}
}
extern "C" bool

View file

@ -602,6 +602,12 @@ Lardon3DFeatureStoreResult lardon3d_feature_store_publish_v2(
}
unlink(temporary);
bool durable = sync_directory(prefix);
#ifdef LARDON3D_FEATURE_STORE_TESTING
const char *fail_sync = getenv("LARDON3D_TEST_FEATURE_FAIL_DIRECTORY_SYNC");
if (fail_sync && strcmp(fail_sync, "1") == 0) {
durable = false;
}
#endif
return register_published_feature_set(
state, image_id, producer_task_id, &image_asset, kind, version, fingerprint, descriptor_type,
descriptor_dimension, features, file_hash, relative, file_size, durable, feature_set);

View file

@ -15,6 +15,9 @@
#include <lardon3d/project.h>
#include <lardon3d/task_queue.h>
#include "opencv_task_thread_control.h"
#include "task_internal.h"
typedef struct {
char project_path[PATH_MAX];
Lardon3DProjectDb *database;
@ -125,13 +128,14 @@ static bool load_validated_source(const Lardon3DFeatureTaskContext *context, cha
return file_hash(path, actual) && memcmp(actual, asset.sha256, 32) == 0;
}
static bool run(Lardon3DTask *task, void *userdata) {
static bool run_body(Lardon3DTask *task, void *userdata, size_t *durable_items) {
Lardon3DFeatureTaskContext *context = userdata;
*durable_items = 0;
if (!lardon3d_task_checkpoint(task)) {
return false;
}
struct timespec begin;
(void)clock_gettime(CLOCK_MONOTONIC, &begin);
struct timespec begin = {0};
bool timing_known = clock_gettime(CLOCK_MONOTONIC, &begin) == 0;
unsigned char fingerprint[32];
lardon3d_feature_extractor_parameter_fingerprint(
&(Lardon3DFeatureExtractorParameters){context->parameters.max_features,
@ -186,13 +190,47 @@ static bool run(Lardon3DTask *task, void *userdata) {
published != LARDON3D_FEATURE_STORE_PUBLISHED_NOT_DURABLE) {
return lardon3d_task_fail(task, "Publication Feature Store impossible.");
}
struct timespec end;
(void)clock_gettime(CLOCK_MONOTONIC, &end);
(void)lardon3d_resource_governor_record_batch(context->governor, LARDON3D_RESOURCE_TASK_CPU, 1,
elapsed_ns(begin, end), 0);
*durable_items = published == LARDON3D_FEATURE_STORE_OK ? 1 : 0;
struct timespec end = {0};
timing_known = timing_known && clock_gettime(CLOCK_MONOTONIC, &end) == 0;
if (*durable_items > 0 && timing_known) {
(void)lardon3d_resource_governor_record_batch(
context->governor, LARDON3D_RESOURCE_TASK_CPU, *durable_items,
elapsed_ns(begin, end), 0);
}
return lardon3d_task_set_progress(task, 100, "Feature Set publié.");
}
static bool run(Lardon3DTask *task, void *userdata) {
struct timespec begin = {0};
struct timespec end = {0};
bool timing_known = clock_gettime(CLOCK_MONOTONIC, &begin) == 0;
Lardon3DOpenCvTaskThreadControl threads;
if (!lardon3d_opencv_task_threads_begin(task, 12, &threads)) {
return lardon3d_task_fail(task, "Contrat CPU OpenCV Features invalide.");
}
/* Queue has one callback owner, so this process-wide OpenCV setting cannot
* race another Task. The immutable admission selects 1..12, and this guard
* applies exactly that count before extraction and restores it on every
* return without changing Feature identity. */
size_t durable_items = 0;
bool result = run_body(task, userdata, &durable_items);
if (!lardon3d_opencv_task_threads_end(&threads)) {
(void)lardon3d_task_fail(task, "Restauration OpenCV Features impossible.");
return false;
}
timing_known = timing_known && clock_gettime(CLOCK_MONOTONIC, &end) == 0;
if (result && timing_known) {
/* Only extraction followed by this Task's durable publication contributes
* work. READY reuse, an ALREADY_PRESENT collision, and uncertain directory
* sync remain successful outcomes, but record zero so feedback can neither
* train nor advance the next immutable CPU trial. */
(void)lardon3d_task_internal_record_sequence(
task, elapsed_ns(begin, end), durable_items);
}
return result;
}
static Lardon3DFeatureTaskContext *
make_context(const Lardon3DTaskReconstructionContext *runtime,
const Lardon3DProjectDbFeatureExtractTask *parameters) {
@ -293,11 +331,8 @@ lardon3d_project_create_feature_extract_task(Lardon3DAppState *state, uint64_t i
.memory_bytes_per_item = 512ULL * 1024 * 1024,
.minimum_batch_size = 1,
.maximum_batch_size = 1,
/* Request the audited operational ceiling, not OpenCV's
* mutable current value: Matcher temporarily sets that
* process-wide value to one inside its Queue callback. The
* Governor reduces twelve to the host-reserved CPU budget,
* matching the stable startup configuration. */
/* Canonical durable maximum. Governor admission may select
* any validated OpenCV count in 1..12 for this execution. */
.desired_cpu_threads = 12,
.desired_io_slots = 1,
.task_class = LARDON3D_RESOURCE_TASK_CPU};

View file

@ -26,22 +26,38 @@ read_text(const char *path, char *text, size_t capacity)
if (!text || capacity < 2) {
return false;
}
int descriptor = open(path, O_RDONLY | O_CLOEXEC);
int descriptor = open(path, O_RDONLY | O_CLOEXEC | O_NOFOLLOW);
if (descriptor < 0) {
return false;
}
ssize_t count;
do {
count = read(descriptor, text, capacity - 1);
} while (count < 0 && errno == EINTR);
bool success = count >= 0 && close(descriptor) == 0;
size_t total = 0;
bool success = true;
while (total + 1 < capacity) {
ssize_t count = read(descriptor, text + total, capacity - total - 1);
if (count < 0 && errno == EINTR) continue;
if (count < 0) {
success = false;
break;
}
if (count == 0) break;
total += (size_t)count;
}
if (success && total + 1 == capacity) {
char extra;
ssize_t count;
do {
count = read(descriptor, &extra, 1);
} while (count < 0 && errno == EINTR);
success = count == 0;
}
if (close(descriptor) != 0) success = false;
if (!success) {
return false;
}
text[(size_t)count] = '\0';
while (count > 0 && (text[(size_t)count - 1] == '\n'
|| text[(size_t)count - 1] == '\r')) {
text[--count] = '\0';
text[total] = '\0';
while (total > 0 && (text[total - 1] == '\n'
|| text[total - 1] == '\r')) {
text[--total] = '\0';
}
return true;
}
@ -49,25 +65,51 @@ read_text(const char *path, char *text, size_t capacity)
static bool
parse_uint64(const char *text, uint64_t *value)
{
if (!text || !text[0] || !value || text[0] == '-') {
if (!text || !text[0] || !value) {
return false;
}
errno = 0;
char *end;
unsigned long long parsed = strtoull(text, &end, 0);
if (errno != 0 || end == text) {
return false;
const unsigned char *cursor = (const unsigned char *)text;
uint64_t parsed = 0;
size_t digits = 0;
while (*cursor >= '0' && *cursor <= '9') {
uint64_t digit = (uint64_t)(*cursor - '0');
if (parsed > (UINT64_MAX - digit) / 10U) return false;
parsed = parsed * 10U + digit;
++cursor;
++digits;
}
while (*end == ' ' || *end == '\t' || *end == '\n' || *end == '\r') {
++end;
if (digits == 0) return false;
while (*cursor == ' ' || *cursor == '\t' || *cursor == '\n'
|| *cursor == '\r' || *cursor == '\f' || *cursor == '\v') {
++cursor;
}
if (*end) {
return false;
}
*value = (uint64_t)parsed;
if (*cursor) return false;
*value = parsed;
return true;
}
static bool
shared_memory_evidence(
const Lardon3DHardwareProfile *profile,
uint64_t vram_total,
bool gtt_known,
uint64_t gtt_total
)
{
if (profile->memory_total_bytes == 0) return false;
const uint64_t two_gibibytes = UINT64_C(2) * 1024 * 1024 * 1024;
bool conservatively_small = vram_total <= two_gibibytes
&& vram_total <= profile->memory_total_bytes / 8;
bool system_scale_gtt = gtt_known
&& gtt_total >= profile->memory_total_bytes / 4
&& vram_total <= gtt_total / 2;
/* WHY: amdgpu exposes a small stolen/dedicated VRAM aperture even for an
* integrated GPU. Treating that positive number as separate free memory
* undercharges host RAM. A low-VRAM uncertain device may conservatively
* become UMA; the reverse error could violate the 3 GiB/2 GiB host floors. */
return conservatively_small || system_scale_gtt;
}
static const char *
vendor_name(const char *vendor)
{
@ -92,9 +134,16 @@ lardon3d_hardware_profile_detect_gpu_at_root(
if (!profile || !drm_root) {
return;
}
profile->gpu_available = false;
profile->gpu_drm_card_index = 0;
profile->gpu_memory_known = false;
profile->gpu_uses_shared_memory = false;
profile->gpu_memory_total_bytes = 0;
profile->gpu_name[0] = '\0';
for (unsigned int index = 0; index < 64; ++index) {
char vendor_path[PATH_MAX];
char memory_path[PATH_MAX];
char gtt_path[PATH_MAX];
int vendor_written = snprintf(
vendor_path,
sizeof(vendor_path),
@ -109,9 +158,17 @@ lardon3d_hardware_profile_detect_gpu_at_root(
drm_root,
index
);
int gtt_written = snprintf(
gtt_path,
sizeof(gtt_path),
"%s/card%u/device/mem_info_gtt_total",
drm_root,
index
);
if (vendor_written < 0 || (size_t)vendor_written >= sizeof(vendor_path)
|| memory_written < 0
|| (size_t)memory_written >= sizeof(memory_path)) {
|| (size_t)memory_written >= sizeof(memory_path)
|| gtt_written < 0 || (size_t)gtt_written >= sizeof(gtt_path)) {
continue;
}
char vendor[32];
@ -132,7 +189,16 @@ lardon3d_hardware_profile_detect_gpu_at_root(
&& parse_uint64(memory, &bytes) && bytes > 0) {
profile->gpu_memory_known = true;
profile->gpu_memory_total_bytes = bytes;
char gtt[64];
uint64_t gtt_bytes = 0;
bool gtt_known = read_text(gtt_path, gtt, sizeof(gtt))
&& parse_uint64(gtt, &gtt_bytes) && gtt_bytes > 0;
profile->gpu_uses_shared_memory = shared_memory_evidence(
profile, bytes, gtt_known, gtt_bytes);
} else {
/* Unknown payload capacity must never be treated as an independent
* VRAM budget. Governor can still use the GPU conservatively while
* charging every admitted byte to MemAvailable. */
profile->gpu_uses_shared_memory = true;
}
return;

View file

@ -2,6 +2,7 @@
#include <chrono>
#include <cmath>
#include <cstring>
#include <memory>
#include <new>
#include <vector>
@ -20,11 +21,49 @@ extern "C" {
#include <lardon3d/matcher.h>
#include "matcher_internal.h"
#include "orb_vulkan_backend_internal.h"
}
static const char matcher_orb_str[] = "orb_bf";
static const char matcher_sift_str[] = "sift_bf";
static const char matcher_rootsift_str[] = "rootsift_bf";
/* The pending object owns only unpublished operational state. GPU submission
* has occurred before it escapes begin; the Queue owner later consumes it into
* the canonical staged Match File or discards it without DB mutation. */
struct Lardon3DMatcherPendingVulkanStage {
Lardon3DOrbVulkanBackend *backend = nullptr;
Lardon3DProjectDbFeatureSet a{};
Lardon3DProjectDbFeatureSet b{};
Lardon3DMatcherParams params{};
char project_path[4096]{};
std::vector<Lardon3DOrbTop2> top2;
Lardon3DMatcherStats stats{};
Lardon3DOrbVulkanRequest request{};
bool backend_slot_owned = false;
~Lardon3DMatcherPendingVulkanStage() {
if (backend_slot_owned && backend) {
(void)lardon3d_orb_vulkan_internal_top2_discard(backend,
&request);
}
}
};
struct MatcherStagedFileGuard {
Lardon3DMatcherStagedResult *staged = nullptr;
int fd = -1;
bool retained = false;
~MatcherStagedFileGuard() {
if (fd >= 0) {
(void)close(fd);
}
if (!retained && staged) {
lardon3d_matcher_discard_staged(staged);
}
}
};
static uint64_t elapsed_ns(std::chrono::steady_clock::time_point start) {
auto elapsed = std::chrono::steady_clock::now() - start;
@ -525,6 +564,10 @@ extern "C" Lardon3DMatcherResult lardon3d_matcher_run_with_backend(
return LARDON3D_MATCHER_FAILED;
} catch (const std::bad_alloc &) {
return LARDON3D_MATCHER_FAILED;
} catch (...) {
/* Public C boundary: mutex/system/runtime exceptions are operational
* failures and must never escape into the C Task callback. */
return LARDON3D_MATCHER_FAILED;
}
}
@ -556,7 +599,7 @@ extern "C" void lardon3d_matcher_discard_staged(Lardon3DMatcherStagedResult *sta
std::memset(staged, 0, sizeof(*staged));
}
extern "C" Lardon3DMatcherResult lardon3d_matcher_stage(
static Lardon3DMatcherResult matcher_stage_impl(
const char *project_path,
const Lardon3DProjectDbFeatureSet *feature_set_a,
const Lardon3DProjectDbFeatureSet *feature_set_b,
@ -591,7 +634,217 @@ extern "C" Lardon3DMatcherResult lardon3d_matcher_stage(
return computed;
}
extern "C" Lardon3DMatcherResult lardon3d_matcher_match_and_publish_with_backend(
extern "C" Lardon3DMatcherResult lardon3d_matcher_stage(
const char *project_path,
const Lardon3DProjectDbFeatureSet *feature_set_a,
const Lardon3DProjectDbFeatureSet *feature_set_b,
const Lardon3DMatcherParams *params, Lardon3DOrbVulkanBackend *backend,
Lardon3DMatcherStagedResult *staged) {
if (staged) std::memset(staged, 0, sizeof(*staged));
try {
return matcher_stage_impl(project_path, feature_set_a, feature_set_b,
params, backend, staged);
} catch (...) {
if (staged) lardon3d_matcher_discard_staged(staged);
return LARDON3D_MATCHER_FAILED;
}
}
static Lardon3DMatcherResult matcher_begin_vulkan_stage_impl(
const char *project_path, const Lardon3DProjectDbFeatureSet *a,
const Lardon3DProjectDbFeatureSet *b, const Lardon3DMatcherParams *params,
Lardon3DOrbVulkanBackend *backend, Lardon3DMatcherPendingVulkanStage **out,
bool *backend_fault) {
if (out) *out = nullptr;
if (backend_fault) *backend_fault = false;
if (!project_path || !a || !b || !params || !backend || !out ||
!backend_fault ||
params->kind != LARDON3D_MATCHER_ORB_BF ||
!std::isfinite(params->ratio_threshold) ||
params->ratio_threshold <= 0.0F || params->ratio_threshold >= 1.0F ||
a->descriptor_type != LARDON3D_FEATURE_DESCRIPTOR_U8 ||
b->descriptor_type != LARDON3D_FEATURE_DESCRIPTOR_U8 ||
a->descriptor_dimension != 32 || b->descriptor_dimension != 32 ||
!lardon3d_orb_vulkan_should_use(a->feature_count, b->feature_count)) {
return LARDON3D_MATCHER_INVALID_ARGUMENT;
}
std::unique_ptr<Lardon3DMatcherPendingVulkanStage> pending(
new (std::nothrow) Lardon3DMatcherPendingVulkanStage());
if (!pending) return LARDON3D_MATCHER_FAILED;
pending->backend = backend;
pending->a = *a;
pending->b = *b;
pending->params = *params;
pending->top2.resize(a->feature_count);
int written = std::snprintf(pending->project_path,
sizeof(pending->project_path), "%s",
project_path);
if (written <= 0 ||
static_cast<size_t>(written) >= sizeof(pending->project_path)) {
return LARDON3D_MATCHER_IO_ERROR;
}
FeatureReaderPair readers;
Lardon3DFeatureFileMetadata ma, mb;
auto start = std::chrono::steady_clock::now();
if (lardon3d_feature_reader_open(project_path, a, &readers.a, &ma) != LARDON3D_FEATURE_STORE_OK ||
lardon3d_feature_reader_open(project_path, b, &readers.b, &mb) !=
LARDON3D_FEATURE_STORE_OK) {
return LARDON3D_MATCHER_IO_ERROR;
}
std::vector<unsigned char> da, db;
if (!fill_descriptors_u8(da, readers.a, a->feature_count, 32) ||
!fill_descriptors_u8(db, readers.b, b->feature_count, 32)) {
return LARDON3D_MATCHER_IO_ERROR;
}
pending->stats.descriptor_read_ns = elapsed_ns(start);
pending->stats.feature_count_a = a->feature_count;
pending->stats.feature_count_b = b->feature_count;
if (lardon3d_orb_vulkan_internal_top2_begin(
backend, da.data(), a->feature_count, db.data(), b->feature_count,
&pending->request
) != LARDON3D_ORB_VULKAN_OK) {
*backend_fault = true;
return LARDON3D_MATCHER_FAILED;
}
pending->backend_slot_owned = true;
*out = pending.release();
return LARDON3D_MATCHER_OK;
}
extern "C" Lardon3DMatcherResult lardon3d_matcher_begin_vulkan_stage(
const char *project_path, const Lardon3DProjectDbFeatureSet *a,
const Lardon3DProjectDbFeatureSet *b, const Lardon3DMatcherParams *params,
Lardon3DOrbVulkanBackend *backend,
Lardon3DMatcherPendingVulkanStage **out, bool *backend_fault) {
if (out) *out = nullptr;
if (backend_fault) *backend_fault = false;
try {
return matcher_begin_vulkan_stage_impl(
project_path, a, b, params, backend, out, backend_fault);
} catch (...) {
/* All potentially throwing C++ work precedes the no-throw C backend
* begin call or follows a successful return. An exception here is a
* local allocation/reader fault, never backend-health evidence. */
return LARDON3D_MATCHER_FAILED;
}
}
static Lardon3DMatcherResult matcher_finish_vulkan_stage_impl(
Lardon3DMatcherPendingVulkanStage *pending,
Lardon3DMatcherStagedResult *staged, bool *backend_fault) {
if (backend_fault) *backend_fault = false;
if (!pending || !staged || !backend_fault) {
return LARDON3D_MATCHER_INVALID_ARGUMENT;
}
std::unique_ptr<Lardon3DMatcherPendingVulkanStage> owned(pending);
std::memset(staged, 0, sizeof(*staged));
auto start = std::chrono::steady_clock::now();
Lardon3DOrbVulkanResult finished =
lardon3d_orb_vulkan_internal_top2_finish(
pending->backend, &pending->request, pending->top2.data(),
pending->top2.size());
/* Internal finish consumes this exact request on every active-handle
* result; a backend session failure separately invalidates all slots. */
pending->backend_slot_owned = false;
if (finished != LARDON3D_ORB_VULKAN_OK) {
*backend_fault = true;
return LARDON3D_MATCHER_FAILED;
}
pending->stats.knn_ns = elapsed_ns(start);
pending->stats.used_vulkan = true;
pending->stats.knn_query_count = pending->a.feature_count;
std::vector<MatchEntry> filtered;
filtered.reserve(pending->a.feature_count);
start = std::chrono::steady_clock::now();
for (uint32_t i = 0; i < pending->a.feature_count; ++i) {
MatchEntry entry;
if (accept_orb_top2(pending->top2[i], i, pending->params.ratio_threshold, &entry)) filtered.push_back(entry);
}
pending->stats.filter_ns = elapsed_ns(start);
start = std::chrono::steady_clock::now();
std::sort(filtered.begin(), filtered.end());
for (size_t i = 1; i < filtered.size(); ++i) {
if (filtered[i].query_idx == filtered[i - 1].query_idx) {
return LARDON3D_MATCHER_FAILED;
}
}
if (filtered.size() > LARDON3D_MATCH_FILE_MAX_MATCHES) {
return LARDON3D_MATCHER_FAILED;
}
pending->stats.match_count = (uint32_t)filtered.size();
pending->stats.canonicalize_ns = elapsed_ns(start);
char assets[4096], matches[4096];
if (!join_path(assets, pending->project_path, "assets") ||
!ensure_directory(assets) || !join_path(matches, assets, "matches") ||
!ensure_directory(matches)) {
return LARDON3D_MATCHER_IO_ERROR;
}
int written = std::snprintf(staged->temporary_path,
sizeof(staged->temporary_path),
"%s/.match-XXXXXX", matches);
if (written <= 0 ||
static_cast<size_t>(written) >= sizeof(staged->temporary_path)) {
staged->temporary_path[0] = '\0';
return LARDON3D_MATCHER_IO_ERROR;
}
int fd = mkstemp(staged->temporary_path);
if (fd < 0) {
staged->temporary_path[0] = '\0';
return LARDON3D_MATCHER_IO_ERROR;
}
MatcherStagedFileGuard file_guard{staged, fd, false};
std::vector<Lardon3DMatchFileEntry> entries(filtered.size());
for (size_t i = 0; i < filtered.size(); ++i) {
entries[i] = {(uint32_t)filtered[i].query_idx,
(uint32_t)filtered[i].train_idx, filtered[i].distance};
}
Lardon3DMatchFileResult wr = lardon3d_match_file_write(
fd, 1, pending->a.descriptor_dimension, pending->a.feature_set_id,
pending->b.feature_set_id, entries.data(),
static_cast<uint32_t>(entries.size()));
if (wr == LARDON3D_MATCH_FILE_OK && fsync(fd) != 0) wr = LARDON3D_MATCH_FILE_IO_ERROR;
if (close(fd) != 0) wr = LARDON3D_MATCH_FILE_IO_ERROR;
file_guard.fd = -1;
if (wr != LARDON3D_MATCH_FILE_OK) {
return LARDON3D_MATCHER_IO_ERROR;
}
staged->stats = pending->stats;
file_guard.retained = true;
return LARDON3D_MATCHER_OK;
}
extern "C" Lardon3DMatcherResult lardon3d_matcher_finish_vulkan_stage(
Lardon3DMatcherPendingVulkanStage *pending,
Lardon3DMatcherStagedResult *staged, bool *backend_fault) {
if (backend_fault) *backend_fault = false;
try {
return matcher_finish_vulkan_stage_impl(
pending, staged, backend_fault);
} catch (...) {
if (staged) lardon3d_matcher_discard_staged(staged);
/* The backend C transaction cannot throw. Exceptions after its
* successful return belong to local filtering/staging and must not
* disable a healthy shared device. */
return LARDON3D_MATCHER_FAILED;
}
}
extern "C" void lardon3d_matcher_discard_vulkan_stage(Lardon3DMatcherPendingVulkanStage *pending) {
if (!pending) return;
try {
std::unique_ptr<Lardon3DMatcherPendingVulkanStage> owned(pending);
if (pending->backend_slot_owned) {
(void)lardon3d_orb_vulkan_internal_top2_discard(
pending->backend, &pending->request);
pending->backend_slot_owned = false;
}
} catch (...) {
/* C cancellation seam: the pending destructor performs the same
* bounded discard attempt and no C++ exception may escape. */
}
}
static Lardon3DMatcherResult matcher_match_and_publish_with_backend_impl(
const char *project_path,
Lardon3DProjectDb *database,
const Lardon3DProjectDbCandidatePair *pair,
@ -822,6 +1075,38 @@ extern "C" Lardon3DMatcherResult lardon3d_matcher_match_and_publish_with_backend
return LARDON3D_MATCHER_OK;
}
extern "C" Lardon3DMatcherResult lardon3d_matcher_match_and_publish_with_backend(
const char *project_path,
Lardon3DProjectDb *database,
const Lardon3DProjectDbCandidatePair *pair,
const Lardon3DProjectDbFeatureSet *feature_set_a,
const Lardon3DProjectDbFeatureSet *feature_set_b,
const Lardon3DMatcherParams *params,
Lardon3DOrbVulkanBackend *backend,
Lardon3DProjectDbMatchResult *result,
Lardon3DMatcherStats *profile) {
try {
return matcher_match_and_publish_with_backend_impl(
project_path, database, pair, feature_set_a, feature_set_b, params,
backend, result, profile);
} catch (...) {
if (profile) std::memset(profile, 0, sizeof(*profile));
if (result) std::memset(result, 0, sizeof(*result));
return LARDON3D_MATCHER_FAILED;
}
}
struct MatcherPublicationStageGuard {
Lardon3DMatcherStagedResult *staged;
~MatcherPublicationStageGuard() {
matcher_publication_stage = nullptr;
/* Publication either consumed the path or left it operation-owned.
* Both failure and exception paths end that ownership here. */
lardon3d_matcher_discard_staged(staged);
}
};
extern "C" Lardon3DMatcherResult lardon3d_matcher_publish_staged(
const char *project_path, Lardon3DProjectDb *database,
const Lardon3DProjectDbCandidatePair *pair,
@ -832,15 +1117,19 @@ extern "C" Lardon3DMatcherResult lardon3d_matcher_publish_staged(
if (!staged || staged->temporary_path[0] == '\0' || matcher_publication_stage) {
return LARDON3D_MATCHER_INVALID_ARGUMENT;
}
matcher_publication_stage = staged;
Lardon3DMatcherResult published = lardon3d_matcher_match_and_publish_with_backend(
project_path, database, pair, feature_set_a, feature_set_b, params, nullptr,
result, nullptr);
matcher_publication_stage = nullptr;
/* Reuse and validation failures can return before the normal publication
* path takes ownership. In all cases the operation-owned temp ends here. */
lardon3d_matcher_discard_staged(staged);
return published;
try {
matcher_publication_stage = staged;
MatcherPublicationStageGuard guard{staged};
return lardon3d_matcher_match_and_publish_with_backend(
project_path, database, pair, feature_set_a, feature_set_b, params,
nullptr, result, nullptr);
} catch (...) {
/* The guard normally handles cleanup. This catch also protects
* construction-time failures before it exists. */
matcher_publication_stage = nullptr;
lardon3d_matcher_discard_staged(staged);
return LARDON3D_MATCHER_FAILED;
}
}
extern "C" Lardon3DMatcherResult lardon3d_matcher_match_and_publish(

View file

@ -1,6 +1,8 @@
#ifndef LARDON3D_MATCHER_INTERNAL_H
#define LARDON3D_MATCHER_INTERNAL_H
#include <stdbool.h>
#include <lardon3d/matcher.h>
enum { LARDON3D_MATCHER_STAGED_PATH_CAPACITY = 4096 };
@ -10,25 +12,53 @@ typedef struct {
Lardon3DMatcherStats stats;
} Lardon3DMatcherStagedResult;
typedef struct Lardon3DMatcherPendingVulkanStage Lardon3DMatcherPendingVulkanStage;
#ifdef __cplusplus
extern "C" {
#endif
#if defined(__GNUC__) || defined(__clang__)
#define LARDON3D_MATCHER_INTERNAL_VISIBILITY __attribute__((visibility("hidden")))
#else
#define LARDON3D_MATCHER_INTERNAL_VISIBILITY
#endif
/* Computes one pair into an operation-owned temporary Match File without
* publishing an asset or mutating Project DB. The owner must either publish
* the stage or discard it; this split is what permits deterministic parallel
* compute followed by ordered, single-owner durable publication. */
Lardon3DMatcherResult lardon3d_matcher_stage(
LARDON3D_MATCHER_INTERNAL_VISIBILITY Lardon3DMatcherResult lardon3d_matcher_stage(
const char *project_path,
const Lardon3DProjectDbFeatureSet *feature_set_a,
const Lardon3DProjectDbFeatureSet *feature_set_b,
const Lardon3DMatcherParams *params, Lardon3DOrbVulkanBackend *backend,
Lardon3DMatcherStagedResult *staged);
/* Begin transfers one exact request into the backend and returns one
* operation-owned pending handle. Finish consumes every non-null handle on
* every result; discard is the cancellation path. backend_fault is mandatory,
* starts false, and becomes true only when the corresponding Vulkan begin or
* finish transaction fails. Local feature I/O, allocation, post-processing or
* Match File staging errors remain ordinary Matcher failures so the Task does
* not poison shared backend health. Neither private stage nor any partial GPU
* evidence may be published by these seams. */
LARDON3D_MATCHER_INTERNAL_VISIBILITY Lardon3DMatcherResult
lardon3d_matcher_begin_vulkan_stage(
const char *, const Lardon3DProjectDbFeatureSet *,
const Lardon3DProjectDbFeatureSet *, const Lardon3DMatcherParams *,
Lardon3DOrbVulkanBackend *, Lardon3DMatcherPendingVulkanStage **, bool *);
LARDON3D_MATCHER_INTERNAL_VISIBILITY Lardon3DMatcherResult
lardon3d_matcher_finish_vulkan_stage(
Lardon3DMatcherPendingVulkanStage *, Lardon3DMatcherStagedResult *, bool *);
LARDON3D_MATCHER_INTERNAL_VISIBILITY void
lardon3d_matcher_discard_vulkan_stage(Lardon3DMatcherPendingVulkanStage *);
/* Publishes a successful stage using the existing Match Result identity,
* reuse, repair, and atomic asset rules. This function consumes the stage on
* every return path and must be called only by the Task callback owner. */
Lardon3DMatcherResult lardon3d_matcher_publish_staged(
LARDON3D_MATCHER_INTERNAL_VISIBILITY Lardon3DMatcherResult
lardon3d_matcher_publish_staged(
const char *project_path, Lardon3DProjectDb *database,
const Lardon3DProjectDbCandidatePair *pair,
const Lardon3DProjectDbFeatureSet *feature_set_a,
@ -36,7 +66,10 @@ Lardon3DMatcherResult lardon3d_matcher_publish_staged(
const Lardon3DMatcherParams *params, Lardon3DMatcherStagedResult *staged,
Lardon3DProjectDbMatchResult *result);
void lardon3d_matcher_discard_staged(Lardon3DMatcherStagedResult *staged);
LARDON3D_MATCHER_INTERNAL_VISIBILITY void
lardon3d_matcher_discard_staged(Lardon3DMatcherStagedResult *staged);
#undef LARDON3D_MATCHER_INTERNAL_VISIBILITY
#ifdef __cplusplus
}

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,16 @@
#ifndef LARDON3D_MATCHER_TASK_BENCHMARK_INTERNAL_H
#define LARDON3D_MATCHER_TASK_BENCHMARK_INTERNAL_H
/* This token exists only in the opt-in real-corpus runner and dedicated
* Matcher tests. Production matcher_task.c is compiled without the macro and
* therefore cannot observe or retain the benchmark pipeline selection. */
#ifdef LARDON3D_MATCHER_TASK_BENCHMARK_PIPELINE
#define LARDON3D_MATCHER_TASK_BENCHMARK_SYNCHRONOUS_ENV \
"LARDON3D_BENCHMARK_MATCHER_SYNCHRONOUS_FENCE_V1"
#define LARDON3D_MATCHER_TASK_BENCHMARK_INFLIGHT_ENV \
"LARDON3D_BENCHMARK_MATCHER_INFLIGHT_V1"
#define LARDON3D_MATCHER_TASK_BENCHMARK_BATCH_ENV \
"LARDON3D_BENCHMARK_MATCHER_BATCH_V1"
#endif
#endif

View file

@ -0,0 +1,46 @@
#ifndef LARDON3D_OPENCV_TASK_THREAD_CONTROL_H
#define LARDON3D_OPENCV_TASK_THREAD_CONTROL_H
#include <stdbool.h>
#include <lardon3d/feature_extractor.h>
#include <lardon3d/task.h>
typedef struct {
unsigned int previous;
bool restore_required;
} Lardon3DOpenCvTaskThreadControl;
/* OpenCV owns one process-wide CPU pool. Queue's single callback owner makes
* the change race-free, but configure may mutate before its verification
* fails. Therefore begin attempts rollback on every post-capture failure and
* end restores on every callback result. */
static inline bool lardon3d_opencv_task_threads_begin(
Lardon3DTask *task, unsigned int validated_maximum,
Lardon3DOpenCvTaskThreadControl *control) {
if (!task || !control || validated_maximum == 0) return false;
*control = (Lardon3DOpenCvTaskThreadControl){0};
Lardon3DTaskExecutionContract contract;
if (!lardon3d_task_execution_contract(task, &contract) ||
contract.cpu_threads == 0 || contract.cpu_threads > validated_maximum) {
return false;
}
control->previous = lardon3d_feature_opencv_thread_count();
control->restore_required = true;
if (!lardon3d_feature_opencv_configure_threads(contract.cpu_threads)) {
/* Verification failure is after a possibly successful setNumThreads(). */
(void)lardon3d_feature_opencv_configure_threads(control->previous);
control->restore_required = false;
return false;
}
return true;
}
static inline bool lardon3d_opencv_task_threads_end(
Lardon3DOpenCvTaskThreadControl *control) {
if (!control || !control->restore_required) return false;
control->restore_required = false;
return lardon3d_feature_opencv_configure_threads(control->previous);
}
#endif

View file

@ -0,0 +1,69 @@
#ifndef LARDON3D_OPENCV_TASK_THREAD_GUARD_H
#define LARDON3D_OPENCV_TASK_THREAD_GUARD_H
#include <climits>
extern "C" {
#include <lardon3d/task.h>
}
#include <opencv2/core.hpp>
/* OpenCV's thread count is process-wide. Queue's single execution owner makes
* this scoped set/restore safe for Task callbacks: the admitted count is
* applied for the whole callback and the previous runtime setting is restored
* on success, failure, cancellation, and C++ exception unwinding. */
class Lardon3DOpenCvTaskThreadGuard {
public:
explicit Lardon3DOpenCvTaskThreadGuard(Lardon3DTask *task) noexcept {
try {
Lardon3DTaskExecutionContract contract{};
previous_ = cv::getNumThreads();
previous_known_ = true;
valid_ = lardon3d_task_execution_contract(task, &contract) &&
contract.cpu_threads > 0 && contract.cpu_threads <= INT_MAX;
if (valid_) {
cv::setNumThreads(static_cast<int>(contract.cpu_threads));
valid_ = cv::getNumThreads() == static_cast<int>(contract.cpu_threads);
}
} catch (...) {
valid_ = false;
}
}
~Lardon3DOpenCvTaskThreadGuard() noexcept {
(void)restore();
}
bool restore() noexcept {
if (restored_)
return restore_succeeded_;
if (!previous_known_)
return false;
try {
cv::setNumThreads(previous_ > 0 ? previous_ : 1);
restored_ = true;
restore_succeeded_ =
cv::getNumThreads() == (previous_ > 0 ? previous_ : 1);
return restore_succeeded_;
} catch (...) {
/* C callback boundary: restoration failure cannot escape as C++. */
return false;
}
}
Lardon3DOpenCvTaskThreadGuard(const Lardon3DOpenCvTaskThreadGuard &) = delete;
Lardon3DOpenCvTaskThreadGuard &operator=(
const Lardon3DOpenCvTaskThreadGuard &) = delete;
bool valid() const noexcept { return valid_; }
private:
int previous_{};
bool previous_known_{};
bool valid_{};
bool restored_{};
bool restore_succeeded_{};
};
#endif

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,121 @@
#ifndef LARDON3D_ORB_VULKAN_BACKEND_INTERNAL_H
#define LARDON3D_ORB_VULKAN_BACKEND_INTERNAL_H
#include <stdbool.h>
#include <stdint.h>
#include <lardon3d/orb_vulkan_backend.h>
#ifdef __cplusplus
extern "C" {
#endif
#if defined(__GNUC__) || defined(__clang__)
#define LARDON3D_INTERNAL_VISIBILITY __attribute__((visibility("hidden")))
#else
#define LARDON3D_INTERNAL_VISIBILITY
#endif
typedef struct {
uint64_t serial;
uint64_t submits;
uint64_t completions;
uint64_t submit_cpu_ns;
uint64_t fence_wait_ns;
uint64_t readback_ns;
bool gpu_timestamps_available;
uint64_t gpu_execution_ns;
uint64_t starvation_ns;
uint64_t failures;
uint64_t discards;
bool slot_pending;
uint32_t pending_slots;
uint32_t retained_capacity;
uint64_t retained_payload_bytes;
bool sequence_capacity_active;
} Lardon3DOrbVulkanTelemetry;
enum {
LARDON3D_ORB_VULKAN_MAX_INFLIGHT = 2,
/* Explicit host-visible Vulkan buffer payload. Each slot owns two 8192x32
* descriptor inputs and one 8192x4x32-bit readback buffer. Device,
* pipeline, layouts and cache are immutable shared objects with opaque
* driver allocations; no invented byte charge is assigned to them. */
LARDON3D_ORB_VULKAN_FIXED_BYTES = 0,
LARDON3D_ORB_VULKAN_PER_SLOT_BYTES =
LARDON3D_ORB_VULKAN_PERMANENT_BUFFER_BYTES
};
typedef struct {
uint32_t slot;
uint64_t generation;
} Lardon3DOrbVulkanRequest;
/* Matcher owns one capacity lease for the immutable executing sequence. The
* backend may grow/shrink mapped request payload only while no request is
* pending. End releases any depth-two payload and restores the public/default
* depth-one capacity before the Task crosses its next admission boundary. */
LARDON3D_INTERNAL_VISIBILITY bool
lardon3d_orb_vulkan_internal_begin_sequence(
Lardon3DOrbVulkanBackend *backend,
uint32_t inflight_capacity
);
LARDON3D_INTERNAL_VISIBILITY bool
lardon3d_orb_vulkan_internal_end_sequence(
Lardon3DOrbVulkanBackend *backend
);
/* Up to two private backend-owned requests may be in flight. The handle is the
* exact slot identity plus a nonzero generation: finish/discard can consume
* only that request and stale or mismatched handles never redirect evidence.
* Begin stores the submitted feature counts in the slot; finish never trusts
* fresh caller counts. Every finish result for a valid active handle consumes
* or fails that slot, including invalid output capacity. */
LARDON3D_INTERNAL_VISIBILITY Lardon3DOrbVulkanResult
lardon3d_orb_vulkan_internal_top2_begin(
Lardon3DOrbVulkanBackend *backend,
const unsigned char *descriptors_a,
uint32_t feature_count_a,
const unsigned char *descriptors_b,
uint32_t feature_count_b,
Lardon3DOrbVulkanRequest *request
);
LARDON3D_INTERNAL_VISIBILITY Lardon3DOrbVulkanResult
lardon3d_orb_vulkan_internal_top2_finish(
Lardon3DOrbVulkanBackend *backend,
const Lardon3DOrbVulkanRequest *request,
Lardon3DOrbTop2 *output,
size_t output_capacity
);
LARDON3D_INTERNAL_VISIBILITY Lardon3DOrbVulkanResult
lardon3d_orb_vulkan_internal_top2_discard(
Lardon3DOrbVulkanBackend *backend,
const Lardon3DOrbVulkanRequest *request
);
/* Cumulative counters saturate at UINT64_MAX. The snapshot is protected by
* backend request-state ownership and therefore observes slot state and every
* counter from one instant without widening the public backend-info ABI. */
LARDON3D_INTERNAL_VISIBILITY bool
lardon3d_orb_vulkan_internal_telemetry(
Lardon3DOrbVulkanBackend *backend,
Lardon3DOrbVulkanTelemetry *telemetry
);
#ifdef LARDON3D_ORB_VULKAN_TESTING
/* Inactive-slot saturation seam. It exercises production generation-retire
* branching without billions of submissions and never exists in lardon3d. */
LARDON3D_INTERNAL_VISIBILITY bool
lardon3d_orb_vulkan_internal_test_set_slot_generation(
Lardon3DOrbVulkanBackend *backend,
uint32_t slot,
uint64_t generation
);
#endif
#undef LARDON3D_INTERNAL_VISIBILITY
#ifdef __cplusplus
}
#endif
#endif

View file

@ -5,6 +5,8 @@ extern "C" {
#include <lardon3d/task_queue.h>
}
#include "opencv_task_thread_guard.h"
#include <cstdio>
#include <memory>
#include <new>
@ -119,7 +121,15 @@ bool run_impl(Lardon3DTask *task, void *value) {
}
bool run(Lardon3DTask *task, void *value) noexcept {
try { return run_impl(task, value); }
try {
Lardon3DOpenCvTaskThreadGuard threads(task);
if (!threads.valid())
return lardon3d_task_fail(task, "Contrat CPU OpenCV triage invalide.");
bool result = run_impl(task, value);
if (!threads.restore())
return lardon3d_task_fail(task, "Restauration OpenCV triage impossible.");
return result;
}
catch (const std::bad_alloc &) { return lardon3d_task_fail(task, "Mémoire insuffisante."); }
catch (...) { return lardon3d_task_fail(task, "Erreur interne du triage photo."); }
}

View file

@ -5,6 +5,8 @@ extern "C" {
#include <lardon3d/task_queue.h>
}
#include "opencv_task_thread_guard.h"
#include <cstdio>
#include <new>
@ -84,7 +86,13 @@ bool run_impl(Lardon3DTask *task, Context *context) {
bool run(Lardon3DTask *task, void *value) noexcept {
try {
return run_impl(task, static_cast<Context *>(value));
Lardon3DOpenCvTaskThreadGuard threads(task);
if (!threads.valid())
return lardon3d_task_fail(task, "Contrat CPU OpenCV RAW invalide.");
bool result = run_impl(task, static_cast<Context *>(value));
if (!threads.restore())
return lardon3d_task_fail(task, "Restauration OpenCV RAW impossible.");
return result;
} catch (const std::bad_alloc &) {
return lardon3d_task_fail(task, "Mémoire insuffisante pour le développement RAW.");
} catch (...) {

File diff suppressed because it is too large Load diff

View file

@ -2,10 +2,396 @@
#define LARDON3D_RESOURCE_GOVERNOR_INTERNAL_H
#include <stdbool.h>
#include <stddef.h>
#include <stdint.h>
#include <time.h>
#include <lardon3d/resource_governor.h>
#include <lardon3d/task.h>
#ifdef __cplusplus
extern "C" {
#endif
enum {
LARDON3D_RESOURCE_CAPABILITY_MAX = 4,
LARDON3D_RESOURCE_DIAGNOSTIC_REASON_CAPACITY = 96,
LARDON3D_RESOURCE_CPU_MAX = 1024,
LARDON3D_RESOURCE_CPU_MASK_WORDS = LARDON3D_RESOURCE_CPU_MAX / 64,
};
typedef enum {
LARDON3D_RESOURCE_BACKEND_FIXED = 0,
LARDON3D_RESOURCE_BACKEND_CPU = 1,
LARDON3D_RESOURCE_BACKEND_ORB_VULKAN = 2,
/* Diagnostics only: one admitted GPU sequence completed whole pairs on
* both backends after an operational Vulkan failure. Never a capability. */
LARDON3D_RESOURCE_BACKEND_MIXED = 3,
} Lardon3DResourceBackend;
typedef struct {
Lardon3DResourceEstimate estimate;
Lardon3DResourceBackend backend;
/* Maximum validated simultaneous requests. A zero minimum means the
* established fixed-capability shorthand minimum==maximum; adaptive
* capabilities state the lower bound explicitly. The canonical durable
* estimate remains untouched while per-inflight operational GPU bytes are
* reconstructed privately for each sequence. */
size_t inflight_limit;
size_t minimum_inflight_limit;
uint64_t gpu_memory_bytes_per_inflight;
unsigned int helper_limit;
bool preferred;
bool cpu_reducible;
bool batch_adaptive;
/* Current ORB Vulkan batch trials use a longer observation window than
* generic CPU adaptation. This private operational property is
* reconstructed with the capability and is never durable/scientific. */
bool sustained_gpu_batch_feedback;
bool inflight_adaptive;
bool requires_runtime_backend;
} Lardon3DTaskCapability;
typedef struct {
size_t count;
Lardon3DTaskCapability capabilities[LARDON3D_RESOURCE_CAPABILITY_MAX];
} Lardon3DTaskCapabilityEnvelope;
typedef struct {
size_t capability_index;
Lardon3DTaskCapability capability;
/* Exact per-sequence estimate used only for reservation. The envelope's
* canonical maximums stay intact for next-sequence feedback. */
Lardon3DResourceEstimate reservation_estimate;
Lardon3DResourceDecision decision;
/* Frozen operational value selected for this sequence. */
size_t inflight_limit;
Lardon3DResourcePressure pressure;
char reason[LARDON3D_RESOURCE_DIAGNOSTIC_REASON_CAPACITY];
} Lardon3DResourceCapabilitySelection;
typedef struct {
bool memory_available_known;
uint64_t memory_available_bytes;
bool memory_psi_some_known;
uint32_t memory_psi_some_basis_points;
bool memory_psi_full_known;
uint32_t memory_psi_full_basis_points;
bool io_psi_some_known;
uint32_t io_psi_some_basis_points;
bool io_psi_full_known;
uint32_t io_psi_full_basis_points;
bool swap_delta_known;
uint64_t swap_pages_in_delta;
uint64_t swap_pages_out_delta;
bool compute_pool_utilization_known;
uint32_t compute_pool_utilization_basis_points;
bool gpu_busy_known;
uint32_t gpu_busy_basis_points;
bool process_rss_known;
uint64_t process_rss_bytes;
bool process_peak_rss_known;
uint64_t process_peak_rss_bytes;
} Lardon3DResourceHostTelemetry;
/* Private raw-input seam used by production's bounded proc/sysfs readers and
* deterministic tests. Pointers are borrowed only for the call. A missing
* optional input produces an unknown metric, never a Task failure. */
typedef struct {
const char *proc_stat;
const char *meminfo;
const char *memory_psi;
const char *io_psi;
const char *vmstat;
const char *process_status;
const char *gpu_busy_percent;
} Lardon3DResourceTelemetryRaw;
typedef struct {
uint64_t vulkan_submits;
uint64_t vulkan_completions;
uint64_t vulkan_submit_cpu_ns;
uint64_t vulkan_fence_wait_ns;
uint64_t vulkan_readback_ns;
bool vulkan_gpu_time_known;
uint64_t vulkan_gpu_ns;
uint64_t vulkan_starvation_ns;
uint64_t matcher_cpu_ns;
uint64_t publication_ns;
/* A Vulkan-selected Matcher pair is classified only after its complete
* CPU fallback is durably published. Normal CPU-selected work is not a
* fallback. The sequence bound keeps these counters small; the explicit
* flag still makes injected/overflowed private telemetry fail closed. */
bool fallback_items_saturated;
uint64_t local_ineligible_fallback_items;
uint64_t backend_failure_fallback_items;
uint64_t backend_other_fallback_items;
} Lardon3DResourceExecutionMetrics;
/* Item-level fallback evidence is committed at the exact durable publication
* boundary, independently of end-of-sequence throughput feedback. The enum is
* private because these are operational benchmark classes, not scientific or
* persisted Matcher identities. */
typedef enum {
LARDON3D_RESOURCE_FALLBACK_ITEM_LOCAL_INELIGIBLE = 1,
LARDON3D_RESOURCE_FALLBACK_ITEM_BACKEND_FAILURE,
LARDON3D_RESOURCE_FALLBACK_ITEM_OTHER,
} Lardon3DResourceFallbackItemCause;
typedef struct {
uint64_t serial;
char task_kind[LARDON3D_TASK_KIND_CAPACITY];
uint32_t task_kind_version;
/* `backend` is the immutable selected capability. `actual_backend` is
* execution feedback and may differ only through complete CPU fallback. */
Lardon3DResourceBackend backend;
Lardon3DResourceBackend actual_backend;
bool backend_fallback;
uint64_t previous_wall_time_ns;
size_t items_completed;
uint64_t durable_items_per_second_milli;
size_t batch_size;
size_t inflight_limit;
unsigned int helper_limit;
uint64_t memory_bytes;
uint64_t gpu_memory_bytes;
unsigned int cpu_threads;
unsigned int gpu_slots;
unsigned int io_slots;
Lardon3DResourcePressure pressure;
Lardon3DResourceHostTelemetry host;
Lardon3DResourceExecutionMetrics execution;
char reason[LARDON3D_RESOURCE_DIAGNOSTIC_REASON_CAPACITY];
char backend_reason[LARDON3D_RESOURCE_DIAGNOSTIC_REASON_CAPACITY];
} Lardon3DResourceSequenceDiagnostic;
/* Fixed-size Governor-lifetime evidence. This aggregate makes fast sequences
* countable even when a polling runner observes only the latest diagnostic
* change. Counters saturate, gauges retain extrema, and no per-sequence history
* or scientific state is persisted. */
typedef struct {
bool saturated;
uint64_t admission_count;
uint64_t sequence_count;
uint64_t durable_items;
uint64_t total_wall_time_ns;
uint64_t backend_fallback_sequences;
/* Benchmark evidence must distinguish the scientifically valid complete
* CPU handling of a locally Vulkan-ineligible pair from an unhealthy
* backend. These bounded counters classify exact execution reasons; they
* remain operational Governor telemetry and are never persisted. */
uint64_t backend_ineligible_fallback_sequences;
uint64_t backend_failure_fallback_sequences;
uint64_t backend_other_fallback_sequences;
/* Item counters, unlike the sequence classifiers above, are invariant to
* benchmark batch regrouping. They are summed with the aggregate's shared
* saturation flag and remain fixed-size, operational, and non-persistent. */
uint64_t local_ineligible_fallback_items;
uint64_t backend_failure_fallback_items;
uint64_t backend_other_fallback_items;
uint64_t selected_backend_admissions[LARDON3D_RESOURCE_BACKEND_MIXED + 1];
uint64_t actual_backend_sequences[LARDON3D_RESOURCE_BACKEND_MIXED + 1];
uint64_t contract_change_count;
bool memory_available_known;
uint64_t minimum_memory_available_bytes;
bool gpu_busy_known;
uint32_t maximum_gpu_busy_basis_points;
bool process_rss_known;
uint64_t maximum_process_rss_bytes;
bool process_peak_rss_known;
uint64_t maximum_process_peak_rss_bytes;
uint64_t vulkan_submits;
uint64_t vulkan_completions;
uint64_t vulkan_submit_cpu_ns;
uint64_t vulkan_fence_wait_ns;
uint64_t vulkan_readback_ns;
uint64_t vulkan_gpu_known_sequences;
uint64_t vulkan_gpu_ns;
uint64_t vulkan_starvation_ns;
uint64_t matcher_cpu_ns;
uint64_t publication_ns;
} Lardon3DResourceSequenceAggregate;
typedef struct {
unsigned int cpu_id;
unsigned int package_id;
unsigned int core_id;
} Lardon3DResourceCpuTopologyEntry;
typedef struct {
bool affinity_available;
bool topology_available;
size_t allowed_cpu_count;
unsigned int allowed_cpu_ids[LARDON3D_RESOURCE_CPU_MAX];
size_t topology_entry_count;
Lardon3DResourceCpuTopologyEntry
topology_entries[LARDON3D_RESOURCE_CPU_MAX];
} Lardon3DResourceCpuTopologyInput;
typedef struct {
bool affinity_configured;
bool affinity_attempted;
bool affinity_active;
bool runtime_thread_policy_active;
bool mesa_shader_cache_disabled;
bool externally_constrained;
unsigned int compute_cpu_count;
unsigned int reserved_cpu_count;
uint64_t allowed_mask[LARDON3D_RESOURCE_CPU_MASK_WORDS];
uint64_t compute_mask[LARDON3D_RESOURCE_CPU_MASK_WORDS];
uint64_t reserved_mask[LARDON3D_RESOURCE_CPU_MASK_WORDS];
char reason[LARDON3D_RESOURCE_DIAGNOSTIC_REASON_CAPACITY];
char runtime_thread_policy_reason[
LARDON3D_RESOURCE_DIAGNOSTIC_REASON_CAPACITY];
} Lardon3DResourceCpuPolicyDiagnostic;
typedef enum {
LARDON3D_RESOURCE_DRIVER_POLICY_FAILED = 0,
LARDON3D_RESOURCE_DRIVER_POLICY_DEFAULTED,
LARDON3D_RESOURCE_DRIVER_POLICY_INHERITED_SAFE,
LARDON3D_RESOURCE_DRIVER_POLICY_REJECTED_UNSAFE,
} Lardon3DResourceDriverPolicyResult;
bool lardon3d_resource_governor_internal_reserve_capability_available(
Lardon3DResourceGovernor *governor,
const char *task_kind,
uint32_t task_kind_version,
const Lardon3DTaskCapabilityEnvelope *envelope,
Lardon3DResourceCapabilitySelection *selection,
Lardon3DResourceReservation **reservation
);
bool lardon3d_resource_governor_internal_reserve_capability(
Lardon3DResourceGovernor *governor,
const Lardon3DResourceSnapshot *snapshot,
const char *task_kind,
uint32_t task_kind_version,
const Lardon3DTaskCapabilityEnvelope *envelope,
Lardon3DResourceCapabilitySelection *selection,
Lardon3DResourceReservation **reservation
);
bool lardon3d_resource_governor_internal_record_sequence(
Lardon3DResourceGovernor *governor,
const char *task_kind,
uint32_t task_kind_version,
const Lardon3DResourceCapabilitySelection *selection,
uint64_t wall_time_ns,
size_t items_completed
);
bool lardon3d_resource_governor_internal_record_sequence_execution(
Lardon3DResourceGovernor *governor,
const char *task_kind,
uint32_t task_kind_version,
const Lardon3DResourceCapabilitySelection *selection,
uint64_t wall_time_ns,
size_t items_completed,
Lardon3DResourceBackend actual_backend,
const char *backend_reason
);
bool lardon3d_resource_governor_internal_record_sequence_execution_metrics(
Lardon3DResourceGovernor *governor,
const char *task_kind,
uint32_t task_kind_version,
const Lardon3DResourceCapabilitySelection *selection,
uint64_t wall_time_ns,
size_t items_completed,
Lardon3DResourceBackend actual_backend,
const char *backend_reason,
const Lardon3DResourceExecutionMetrics *metrics
);
/* Adds fixed-size ephemeral evidence only. This operation never creates a
* sequence observation, trains throughput feedback, or changes admission.
* `item_count` permits deterministic overflow testing; Task execution passes
* exactly one after each durable fallback publication. */
bool lardon3d_resource_governor_internal_record_fallback_items(
Lardon3DResourceGovernor *governor,
const char *task_kind,
uint32_t task_kind_version,
const Lardon3DResourceCapabilitySelection *selection,
Lardon3DResourceFallbackItemCause cause,
uint64_t item_count
);
bool lardon3d_resource_governor_internal_last_diagnostic(
Lardon3DResourceGovernor *governor,
const char *task_kind,
uint32_t task_kind_version,
Lardon3DResourceSequenceDiagnostic *diagnostic
);
bool lardon3d_resource_governor_internal_diagnostic_since(
Lardon3DResourceGovernor *governor,
const char *task_kind,
uint32_t task_kind_version,
uint64_t after_serial,
Lardon3DResourceSequenceDiagnostic *diagnostic
);
bool lardon3d_resource_governor_internal_sequence_aggregate(
Lardon3DResourceGovernor *governor,
const char *task_kind,
uint32_t task_kind_version,
Lardon3DResourceSequenceAggregate *aggregate
);
bool lardon3d_resource_governor_internal_format_diagnostic(
const Lardon3DResourceSequenceDiagnostic *diagnostic,
char *text,
size_t capacity
);
bool lardon3d_resource_governor_internal_sample_telemetry_raw(
Lardon3DResourceGovernor *governor,
const Lardon3DResourceTelemetryRaw *raw,
Lardon3DResourceHostTelemetry *telemetry
);
/* Reads only the retained DRM card identity below root. No card scan or
* fallback is permitted; O_NOFOLLOW and the production strict parser apply. */
bool lardon3d_resource_governor_internal_read_gpu_busy_at_root(
const char *root,
unsigned int card_index,
uint32_t *basis_points
);
bool lardon3d_resource_governor_internal_set_backend_available(
Lardon3DResourceGovernor *governor,
Lardon3DResourceBackend backend,
bool available
);
bool lardon3d_resource_governor_internal_capability_hardware_safe(
Lardon3DResourceGovernor *governor,
const Lardon3DTaskCapability *capability
);
/* Governor owns the bounded topology snapshot and compute mask. Queue calls
* apply only from its sole heavy-compute worker; callers/main threads must not
* be constrained. Failure is observable and leaves Task scientific state
* untouched. */
bool lardon3d_resource_governor_internal_cpu_policy(
Lardon3DResourceGovernor *governor,
Lardon3DResourceCpuPolicyDiagnostic *diagnostic
);
bool lardon3d_resource_governor_internal_apply_worker_affinity(
Lardon3DResourceGovernor *governor
);
/* Must run on the startup thread before any application pthread is created.
* An absent Mesa setting is defaulted to the safe value; an explicit safe
* value is retained, while false/malformed values are rejected rather than
* overwritten. The policy is harmless on non-Mesa drivers and is operational,
* never scientific identity or persistence. */
Lardon3DResourceDriverPolicyResult
lardon3d_resource_governor_internal_configure_driver_policy(void);
/* Deterministic private injection seam. It replaces only ephemeral topology
* policy and is rejected while CPU reservations are active. */
bool lardon3d_resource_governor_internal_configure_cpu_topology(
Lardon3DResourceGovernor *governor,
const Lardon3DResourceCpuTopologyInput *input
);
void lardon3d_resource_governor_internal_force_worker_affinity_failure(
Lardon3DResourceGovernor *governor,
bool force_failure
);
/* Private parser seam for the exact bounded sysfs reader. The file must
* contain one unsigned decimal token followed only by ASCII whitespace and
* EOF; signs, overflow, tails, and capacity truncation are rejected. */
bool lardon3d_resource_governor_internal_read_topology_value_file(
const char *path,
unsigned int *value
);
typedef struct {
unsigned int pressure_streak;
@ -17,6 +403,10 @@ bool lardon3d_resource_governor_internal_set_next_reservation_id(
Lardon3DResourceGovernor *governor,
uint64_t next_reservation_id
);
bool lardon3d_resource_governor_internal_set_diagnostic_serial(
Lardon3DResourceGovernor *governor,
uint64_t diagnostic_serial
);
bool lardon3d_resource_governor_internal_set_counters(
Lardon3DResourceGovernor *governor,
const Lardon3DResourceGovernorInternalCounters *counters
@ -34,4 +424,8 @@ void lardon3d_resource_governor_internal_force_capture_failure(
bool force_failure
);
#ifdef __cplusplus
}
#endif
#endif

View file

@ -49,6 +49,57 @@ read_file(const char *path, char *buffer, size_t capacity)
return success;
}
static bool
read_exact_decimal_u64(const char *path, uint64_t *value)
{
if (!path || !value) return false;
int descriptor = open(path, O_RDONLY | O_CLOEXEC | O_NOFOLLOW);
if (descriptor < 0) return false;
char buffer[64];
size_t total = 0;
bool success = true;
while (total + 1 < sizeof(buffer)) {
ssize_t count = read(descriptor, buffer + total,
sizeof(buffer) - total - 1);
if (count < 0 && errno == EINTR) continue;
if (count < 0) {
success = false;
break;
}
if (count == 0) break;
total += (size_t)count;
}
if (success && total + 1 == sizeof(buffer)) {
char extra;
ssize_t count;
do {
count = read(descriptor, &extra, 1);
} while (count < 0 && errno == EINTR);
success = count == 0;
}
if (close(descriptor) != 0) success = false;
if (!success || total == 0) return false;
buffer[total] = '\0';
const unsigned char *cursor = (const unsigned char *)buffer;
uint64_t parsed = 0;
size_t digits = 0;
while (*cursor >= '0' && *cursor <= '9') {
uint64_t digit = (uint64_t)(*cursor - '0');
if (parsed > (UINT64_MAX - digit) / 10U) return false;
parsed = parsed * 10U + digit;
++cursor;
++digits;
}
if (digits == 0) return false;
while (*cursor == ' ' || *cursor == '\t' || *cursor == '\n'
|| *cursor == '\r' || *cursor == '\f' || *cursor == '\v') {
++cursor;
}
if (*cursor) return false;
*value = parsed;
return true;
}
static bool
meminfo_bytes(const char *buffer, const char *key, uint64_t *bytes)
{
@ -163,18 +214,12 @@ lardon3d_resource_snapshot_capture_gpu_at_root(
if (written < 0 || (size_t)written >= sizeof(path)) {
return;
}
char buffer[64];
if (!read_file(path, buffer, sizeof(buffer))) {
return;
}
errno = 0;
char *end;
unsigned long long used = strtoull(buffer, &end, 10);
if (errno == 0 && end != buffer
&& (uint64_t)used <= profile->gpu_memory_total_bytes) {
uint64_t used;
if (read_exact_decimal_u64(path, &used)
&& used <= profile->gpu_memory_total_bytes) {
snapshot->gpu_memory_available_known = true;
snapshot->gpu_memory_available_bytes =
profile->gpu_memory_total_bytes - (uint64_t)used;
profile->gpu_memory_total_bytes - used;
}
}

View file

@ -13,6 +13,9 @@
#include <lardon3d/sift_task.h>
#include <lardon3d/task_queue.h>
#include "opencv_task_thread_control.h"
#include "task_internal.h"
typedef struct {
char project_path[PATH_MAX];
Lardon3DProjectDb *database;
@ -92,8 +95,9 @@ static bool source_path(const SiftTaskContext *context, char path[PATH_MAX]) {
memcmp(actual, asset.sha256, 32) == 0;
}
static bool run(Lardon3DTask *task, void *userdata) {
static bool run_body(Lardon3DTask *task, void *userdata, size_t *durable_items) {
SiftTaskContext *context = userdata;
*durable_items = 0;
if (!lardon3d_task_checkpoint(task)) return false;
Lardon3DProjectDbFeatureSet existing;
Lardon3DProjectDbResult found = lardon3d_project_db_find_feature_set(
@ -111,8 +115,8 @@ static bool run(Lardon3DTask *task, void *userdata) {
}
if (found != LARDON3D_PROJECT_DB_NOT_FOUND)
return lardon3d_task_fail(task, "Recherche Feature Store SIFT impossible.");
struct timespec begin;
(void)clock_gettime(CLOCK_MONOTONIC, &begin);
struct timespec begin = {0};
bool timing_known = clock_gettime(CLOCK_MONOTONIC, &begin) == 0;
char path[PATH_MAX];
if (!source_path(context, path)) return lardon3d_task_fail(task, "Asset image corrompu.");
Lardon3DSiftExtractorParameters parameters = extract_parameters(context);
@ -147,13 +151,46 @@ static bool run(Lardon3DTask *task, void *userdata) {
published != LARDON3D_FEATURE_STORE_ALREADY_PRESENT &&
published != LARDON3D_FEATURE_STORE_PUBLISHED_NOT_DURABLE)
return lardon3d_task_fail(task, "Publication SIFT impossible.");
struct timespec end;
(void)clock_gettime(CLOCK_MONOTONIC, &end);
(void)lardon3d_resource_governor_record_batch(
context->governor, LARDON3D_RESOURCE_TASK_CPU, 1, elapsed_ns(begin, end), 0);
*durable_items = published == LARDON3D_FEATURE_STORE_OK ? 1 : 0;
struct timespec end = {0};
timing_known = timing_known && clock_gettime(CLOCK_MONOTONIC, &end) == 0;
if (*durable_items > 0 && timing_known) {
(void)lardon3d_resource_governor_record_batch(
context->governor, LARDON3D_RESOURCE_TASK_CPU, *durable_items,
elapsed_ns(begin, end), 0);
}
return lardon3d_task_set_progress(task, 100, "Feature Set SIFT publié.");
}
static bool run(Lardon3DTask *task, void *userdata) {
struct timespec begin = {0};
struct timespec end = {0};
bool timing_known = clock_gettime(CLOCK_MONOTONIC, &begin) == 0;
Lardon3DOpenCvTaskThreadControl threads;
if (!lardon3d_opencv_task_threads_begin(task, 12, &threads)) {
return lardon3d_task_fail(task, "Contrat CPU OpenCV SIFT invalide.");
}
/* SIFT/RootSIFT consume the immutable admitted 1..12 OpenCV count. Queue's
* single Task owner makes the process-wide set/restore deterministic and
* race-free without changing extractor identity or output semantics. */
size_t durable_items = 0;
bool result = run_body(task, userdata, &durable_items);
if (!lardon3d_opencv_task_threads_end(&threads)) {
(void)lardon3d_task_fail(task, "Restauration OpenCV SIFT impossible.");
return false;
}
timing_known = timing_known && clock_gettime(CLOCK_MONOTONIC, &end) == 0;
if (result && timing_known) {
/* SIFT and RootSIFT retain distinct histories. READY reuse, a durable
* ALREADY_PRESENT collision, and uncertain directory durability are
* successful no-work observations, so they cannot advance either kind's
* next-sequence CPU baseline or trial. */
(void)lardon3d_task_internal_record_sequence(
task, elapsed_ns(begin, end), durable_items);
}
return result;
}
static void finished(const Lardon3DTask *task, void *userdata) {
#ifdef LARDON3D_FEATURE_TASK_TESTING
const char *skip = getenv("LARDON3D_TEST_SIFT_SKIP_FINISHED_CHECKPOINT");
@ -252,9 +289,8 @@ Lardon3DTask *lardon3d_project_create_sift_extract_task(
.memory_bytes_per_item = 1024ULL * 1024 * 1024,
.minimum_batch_size = 1,
.maximum_batch_size = 1,
/* Keep the immutable estimate independent of Matcher's
* temporary process-wide single-thread setting. Governor
* reduction makes this ceiling equal the startup pool. */
/* Canonical durable maximum; the admitted OpenCV count may
* adapt within the validated 1..12 range. */
.desired_cpu_threads = 12,
.desired_io_slots = 1,
.task_class = LARDON3D_RESOURCE_TASK_CPU};

View file

@ -6,6 +6,8 @@
#include <lardon3d/task.h>
#include "task_internal.h"
struct Lardon3DTask {
pthread_mutex_t mutex;
pthread_cond_t condition;
@ -26,6 +28,11 @@ struct Lardon3DTask {
char task_kind[LARDON3D_TASK_KIND_CAPACITY];
uint32_t task_kind_version;
Lardon3DResourceEstimate estimate;
Lardon3DTaskCapabilityEnvelope capability_envelope;
Lardon3DResourceCapabilitySelection pending_selection;
Lardon3DResourceCapabilitySelection selected_capability;
Lardon3DResourceReservation *pending_reservation;
bool has_selected_capability;
Lardon3DTaskExecutionContract contract;
bool has_contract;
bool pause_requested;
@ -34,6 +41,18 @@ struct Lardon3DTask {
Lardon3DResourceGovernor *governor;
Lardon3DResourceReservation *current_reservation;
unsigned int sequence_count;
/* Matcher candidate-pair publication is strictly ascending within one
* Task. This private, non-persisted watermark makes item telemetry
* idempotent in-process without turning it into checkpoint state. */
uint64_t fallback_item_high_water;
bool fallback_items_saturated;
uint64_t local_ineligible_fallback_items;
uint64_t backend_failure_fallback_items;
uint64_t backend_other_fallback_items;
#ifdef LARDON3D_TASK_TESTING
bool test_force_sequence_association_mismatch;
unsigned int test_association_failure_releases;
#endif
};
static bool
@ -83,6 +102,29 @@ copy_text(char *destination, size_t capacity, const char *text)
(void)snprintf(destination, capacity, "%s", text ? text : "");
}
static bool
kind_has_validated_cpu_range(const char *task_kind, uint32_t task_kind_version)
{
if (!task_kind || task_kind_version != 1) {
return false;
}
return strcmp(task_kind, "features.extract") == 0
|| strcmp(task_kind, "features.extract.sift") == 0
|| strcmp(task_kind, "features.extract.rootsift") == 0
|| strcmp(task_kind, "visual_index.update") == 0
|| strcmp(task_kind, "candidate_pair.generate") == 0;
}
static bool
kind_has_validated_batch_range(const char *task_kind, uint32_t version)
{
/* Candidate generation already executes/publishes at every canonical
* 1..64 sequence size. This private bit changes operational pacing only;
* it does not add a scientific or durable identity dimension. */
return task_kind && version == 1
&& strcmp(task_kind, "candidate_pair.generate") == 0;
}
static void
finish_locked(
Lardon3DTask *task,
@ -179,10 +221,355 @@ lardon3d_task_create_typed(
task->task_kind_version = task_kind_version;
}
task->estimate = *estimate;
/* Every Task starts with one honest capability identical to its canonical
* durable estimate. It remains fixed unless kind/version proves a bounded
* operational range or installs alternatives before admission; the
* durable estimate is never mutated. */
task->capability_envelope = (Lardon3DTaskCapabilityEnvelope) {
.count = 1,
.capabilities = {{
.estimate = *estimate,
.backend = LARDON3D_RESOURCE_BACKEND_FIXED,
.inflight_limit = 1,
/* A durable estimate is a fixed operational envelope unless its
* registered kind has proved that its callback consumes a CPU
* range without changing scientific identity. This private bit
* is reconstructed from kind/version and is never persisted. */
.cpu_reducible = typed
&& kind_has_validated_cpu_range(task_kind, task_kind_version),
.batch_adaptive = typed
&& kind_has_validated_batch_range(task_kind, task_kind_version),
}},
};
copy_text(task->message, sizeof(task->message), "En attente.");
return task;
}
bool
lardon3d_task_internal_set_capability_envelope(
Lardon3DTask *task,
const Lardon3DTaskCapabilityEnvelope *envelope
)
{
if (!task || !envelope || envelope->count == 0
|| envelope->count > LARDON3D_RESOURCE_CAPABILITY_MAX) {
return false;
}
for (size_t index = 0; index < envelope->count; ++index) {
const Lardon3DResourceEstimate *estimate =
&envelope->capabilities[index].estimate;
const Lardon3DTaskCapability *capability =
&envelope->capabilities[index];
size_t minimum_inflight = capability->minimum_inflight_limit != 0
? capability->minimum_inflight_limit : capability->inflight_limit;
if (estimate->minimum_batch_size == 0
|| estimate->maximum_batch_size < estimate->minimum_batch_size
|| estimate->desired_cpu_threads == 0
|| capability->inflight_limit == 0
|| minimum_inflight > capability->inflight_limit
|| (capability->sustained_gpu_batch_feedback
&& (!capability->batch_adaptive
|| capability->backend
!= LARDON3D_RESOURCE_BACKEND_ORB_VULKAN))
|| (capability->inflight_adaptive
&& (capability->minimum_inflight_limit == 0
|| capability->gpu_memory_bytes_per_inflight == 0))
|| (capability->gpu_memory_bytes_per_inflight != 0
&& capability->inflight_limit
> UINT64_MAX / capability->gpu_memory_bytes_per_inflight)) {
return false;
}
}
(void)pthread_mutex_lock(&task->mutex);
bool accepted = !task->executing && task->state == TASK_PENDING
&& !task->current_reservation && !task->pending_reservation;
if (accepted) {
/* Task owns this bounded copy for its lifetime. Hardware/backend
* availability remains Governor-owned and is checked at admission. */
task->capability_envelope = *envelope;
}
(void)pthread_mutex_unlock(&task->mutex);
return accepted;
}
bool
lardon3d_task_internal_enable_known_capabilities(Lardon3DTask *task)
{
if (!task) {
return false;
}
(void)pthread_mutex_lock(&task->mutex);
/* Recovery repeats the same private kind/version derivation used at fresh
* creation. Matcher and acquisition hooks may replace this one-capability
* default immediately afterward; the canonical durable estimate remains
* untouched in every case. */
if (task->capability_envelope.count == 1
&& task->capability_envelope.capabilities[0].backend
== LARDON3D_RESOURCE_BACKEND_FIXED) {
task->capability_envelope.capabilities[0].cpu_reducible =
kind_has_validated_cpu_range(
task->task_kind, task->task_kind_version);
task->capability_envelope.capabilities[0].batch_adaptive =
kind_has_validated_batch_range(
task->task_kind, task->task_kind_version);
}
(void)pthread_mutex_unlock(&task->mutex);
return true;
}
bool
lardon3d_task_internal_reserve_available(
Lardon3DTask *task,
Lardon3DResourceGovernor *governor,
Lardon3DResourceDecision *decision,
Lardon3DResourceReservation **reservation
)
{
if (!task || !governor || !decision || !reservation) {
return false;
}
(void)pthread_mutex_lock(&task->mutex);
Lardon3DTaskCapabilityEnvelope envelope = task->capability_envelope;
char task_kind[LARDON3D_TASK_KIND_CAPACITY];
copy_text(task_kind, sizeof(task_kind), task->task_kind[0]
? task->task_kind : "task.untyped");
uint32_t task_kind_version = task->task_kind_version;
(void)pthread_mutex_unlock(&task->mutex);
Lardon3DResourceCapabilitySelection selection;
Lardon3DResourceReservation *created = NULL;
if (!lardon3d_resource_governor_internal_reserve_capability_available(
governor,
task_kind,
task_kind_version,
&envelope,
&selection,
&created
)) {
return false;
}
*decision = selection.decision;
*reservation = created;
if (created) {
(void)pthread_mutex_lock(&task->mutex);
if (task->pending_reservation) {
(void)pthread_mutex_unlock(&task->mutex);
(void)lardon3d_resource_governor_release(governor, created);
*reservation = NULL;
return false;
}
task->pending_selection = selection;
task->pending_reservation = created;
(void)pthread_mutex_unlock(&task->mutex);
}
return true;
}
bool
lardon3d_task_internal_record_sequence_execution(
Lardon3DTask *task,
uint64_t wall_time_ns,
size_t items_completed,
Lardon3DResourceBackend actual_backend,
const char *backend_reason
)
{
return lardon3d_task_internal_record_sequence_execution_metrics(
task, wall_time_ns, items_completed, actual_backend, backend_reason,
NULL);
}
bool
lardon3d_task_internal_record_sequence_execution_metrics(
Lardon3DTask *task,
uint64_t wall_time_ns,
size_t items_completed,
Lardon3DResourceBackend actual_backend,
const char *backend_reason,
const Lardon3DResourceExecutionMetrics *metrics
)
{
if (!task || wall_time_ns == 0 || !backend_reason) {
return false;
}
(void)pthread_mutex_lock(&task->mutex);
bool available = task->executing && task->governor
&& task->has_selected_capability;
Lardon3DResourceGovernor *governor = task->governor;
Lardon3DResourceCapabilitySelection selection = task->selected_capability;
char task_kind[LARDON3D_TASK_KIND_CAPACITY];
copy_text(task_kind, sizeof(task_kind), task->task_kind[0]
? task->task_kind : "task.untyped");
uint32_t task_kind_version = task->task_kind_version;
(void)pthread_mutex_unlock(&task->mutex);
return available
&& lardon3d_resource_governor_internal_record_sequence_execution_metrics(
governor,
task_kind,
task_kind_version,
&selection,
wall_time_ns,
items_completed,
actual_backend,
backend_reason,
metrics
);
}
static void
increment_task_fallback_counter(uint64_t *counter, bool *saturated)
{
if (*counter == UINT64_MAX) {
*saturated = true;
} else {
++*counter;
}
}
bool
lardon3d_task_internal_record_fallback_item(
Lardon3DTask *task,
uint64_t candidate_pair_id,
Lardon3DResourceFallbackItemCause cause
)
{
if (!task || candidate_pair_id == 0
|| cause < LARDON3D_RESOURCE_FALLBACK_ITEM_LOCAL_INELIGIBLE
|| cause > LARDON3D_RESOURCE_FALLBACK_ITEM_OTHER) {
return false;
}
(void)pthread_mutex_lock(&task->mutex);
bool available = task->executing && task->governor
&& task->has_selected_capability
&& task->selected_capability.capability.backend
== LARDON3D_RESOURCE_BACKEND_ORB_VULKAN;
if (!available || candidate_pair_id <= task->fallback_item_high_water) {
bool duplicate = available
&& candidate_pair_id <= task->fallback_item_high_water;
(void)pthread_mutex_unlock(&task->mutex);
return duplicate;
}
Lardon3DResourceGovernor *governor = task->governor;
Lardon3DResourceCapabilitySelection selection = task->selected_capability;
char task_kind[LARDON3D_TASK_KIND_CAPACITY];
copy_text(task_kind, sizeof(task_kind), task->task_kind[0]
? task->task_kind : "task.untyped");
uint32_t task_kind_version = task->task_kind_version;
(void)pthread_mutex_unlock(&task->mutex);
if (!lardon3d_resource_governor_internal_record_fallback_items(
governor, task_kind, task_kind_version, &selection, cause, 1)) {
return false;
}
(void)pthread_mutex_lock(&task->mutex);
/* Queue owns the single callback, so no second recorder can pass the
* watermark concurrently. Retain the check to make the private operation
* idempotent even if a caller retries after a successful commit. */
if (candidate_pair_id > task->fallback_item_high_water) {
task->fallback_item_high_water = candidate_pair_id;
switch (cause) {
case LARDON3D_RESOURCE_FALLBACK_ITEM_LOCAL_INELIGIBLE:
increment_task_fallback_counter(
&task->local_ineligible_fallback_items,
&task->fallback_items_saturated);
break;
case LARDON3D_RESOURCE_FALLBACK_ITEM_BACKEND_FAILURE:
increment_task_fallback_counter(
&task->backend_failure_fallback_items,
&task->fallback_items_saturated);
break;
case LARDON3D_RESOURCE_FALLBACK_ITEM_OTHER:
increment_task_fallback_counter(
&task->backend_other_fallback_items,
&task->fallback_items_saturated);
break;
}
}
(void)pthread_mutex_unlock(&task->mutex);
return true;
}
bool
lardon3d_task_internal_record_sequence(
Lardon3DTask *task,
uint64_t wall_time_ns,
size_t items_completed
)
{
if (!task) {
return false;
}
(void)pthread_mutex_lock(&task->mutex);
Lardon3DResourceBackend selected = task->has_selected_capability
? task->selected_capability.capability.backend
: LARDON3D_RESOURCE_BACKEND_FIXED;
(void)pthread_mutex_unlock(&task->mutex);
return lardon3d_task_internal_record_sequence_execution(
task,
wall_time_ns,
items_completed,
selected,
"selected-backend-completed"
);
}
bool
lardon3d_task_internal_execution_selection(
const Lardon3DTask *task,
Lardon3DResourceCapabilitySelection *selection
)
{
if (!task || !selection) return false;
Lardon3DTask *mutable_task = (Lardon3DTask *)task;
(void)pthread_mutex_lock(&mutable_task->mutex);
bool available = task->executing && task->has_selected_capability
&& task->has_contract;
if (available) {
*selection = task->selected_capability;
}
(void)pthread_mutex_unlock(&mutable_task->mutex);
return available;
}
#ifdef LARDON3D_TASK_TESTING
bool
lardon3d_task_internal_test_force_sequence_association_mismatch(
Lardon3DTask *task
)
{
if (!task) return false;
(void)pthread_mutex_lock(&task->mutex);
bool accepted = task->executing && !is_terminal(task->state);
if (accepted) {
task->test_force_sequence_association_mismatch = true;
task->test_association_failure_releases = 0;
}
(void)pthread_mutex_unlock(&task->mutex);
return accepted;
}
bool
lardon3d_task_internal_test_has_reservation_ownership(Lardon3DTask *task)
{
if (!task) return false;
(void)pthread_mutex_lock(&task->mutex);
bool owned = task->current_reservation || task->pending_reservation;
(void)pthread_mutex_unlock(&task->mutex);
return owned;
}
unsigned int
lardon3d_task_internal_test_association_failure_releases(Lardon3DTask *task)
{
if (!task) return 0;
(void)pthread_mutex_lock(&task->mutex);
unsigned int count = task->test_association_failure_releases;
(void)pthread_mutex_unlock(&task->mutex);
return count;
}
#endif
void
lardon3d_task_destroy(Lardon3DTask *task)
{
@ -232,13 +619,46 @@ lardon3d_task_start(
return false;
}
(void)pthread_mutex_lock(&task->mutex);
if (task->executing || is_terminal(task->state)) {
if (task->executing || is_terminal(task->state)
|| (task->pending_reservation
&& task->pending_reservation != reservation)) {
(void)pthread_mutex_unlock(&task->mutex);
return false;
}
task->executing = true;
task->governor = governor;
task->current_reservation = (Lardon3DResourceReservation *)reservation;
if (task->pending_reservation == reservation) {
task->selected_capability = task->pending_selection;
task->has_selected_capability = true;
task->pending_reservation = NULL;
} else {
/* Direct public callers remain compatible. Their reservation is an
* immutable fixed selection for this sequence. They did not ask the
* private Governor capability chooser, so an adaptive envelope must
* use its durable/minimum inflight rather than advertise an
* unreserved operational maximum. Only Queue-owned admission may
* install a higher adaptive depth. */
const Lardon3DTaskCapability *direct_capability =
&task->capability_envelope.capabilities[0];
size_t direct_inflight = direct_capability->inflight_adaptive
? direct_capability->minimum_inflight_limit
: direct_capability->inflight_limit;
task->selected_capability = (Lardon3DResourceCapabilitySelection) {
.capability = *direct_capability,
.reservation_estimate = direct_capability->estimate,
.decision = {
.kind = LARDON3D_RESOURCE_START,
.batch_size = information.batch_size,
.cpu_threads = information.cpu_threads,
.gpu_slots = information.gpu_slots,
.io_slots = information.io_slots,
},
.inflight_limit = direct_inflight,
.pressure = lardon3d_resource_governor_pressure(governor),
};
task->has_selected_capability = true;
}
task->contract = (Lardon3DTaskExecutionContract) {
.batch_size = information.batch_size,
.memory_bytes = information.memory_bytes,
@ -482,7 +902,6 @@ lardon3d_task_sequence_break(
if (previous) {
(void)lardon3d_resource_governor_release(governor, previous);
}
for (;;) {
/* Vérifier pause et annulation avant chaque tentative d'admission. */
(void)pthread_mutex_lock(&task->mutex);
@ -508,9 +927,9 @@ lardon3d_task_sequence_break(
uint64_t generation = lardon3d_resource_governor_generation(governor);
Lardon3DResourceDecision decision;
Lardon3DResourceReservation *next = NULL;
bool admitted = lardon3d_resource_governor_reserve_available(
bool admitted = lardon3d_task_internal_reserve_available(
task,
governor,
&task->estimate,
&decision,
&next
);
@ -561,7 +980,44 @@ lardon3d_task_sequence_break(
return false;
}
(void)pthread_mutex_lock(&task->mutex);
#ifdef LARDON3D_TASK_TESTING
if (task->test_force_sequence_association_mismatch) {
task->pending_reservation = NULL;
task->test_force_sequence_association_mismatch = false;
}
#endif
task->current_reservation = next;
if (task->pending_reservation != next) {
/* Association validation remains strict. Before releasing the
* rejected reservation, remove every Task ownership marker so
* task_start's common epilogue cannot release it a second time
* or expose a capability without its reservation. */
task->current_reservation = NULL;
task->pending_reservation = NULL;
task->pending_selection = (Lardon3DResourceCapabilitySelection) {0};
task->selected_capability = (Lardon3DResourceCapabilitySelection) {0};
task->has_selected_capability = false;
task->contract = (Lardon3DTaskExecutionContract) {0};
task->has_contract = false;
finish_locked(
task,
TASK_FAILED,
"Sélection de capacité incohérente."
);
(void)pthread_mutex_unlock(&task->mutex);
bool released = lardon3d_resource_governor_release(governor, next);
#ifdef LARDON3D_TASK_TESTING
(void)pthread_mutex_lock(&task->mutex);
if (released) ++task->test_association_failure_releases;
(void)pthread_mutex_unlock(&task->mutex);
#else
(void)released;
#endif
return false;
}
task->selected_capability = task->pending_selection;
task->has_selected_capability = true;
task->pending_reservation = NULL;
task->contract = (Lardon3DTaskExecutionContract) {
.batch_size = information.batch_size,
.memory_bytes = information.memory_bytes,

98
src/task_internal.h Normal file
View file

@ -0,0 +1,98 @@
#ifndef LARDON3D_TASK_INTERNAL_H
#define LARDON3D_TASK_INTERNAL_H
#include <stdbool.h>
#include <stddef.h>
#include <stdint.h>
#include <lardon3d/task.h>
#include "resource_governor_internal.h"
#ifdef __cplusplus
extern "C" {
#endif
/* The envelope is operation-owned policy reconstructed from Task kind/version
* and runtime support. Task copies it; it is neither durable state nor part of
* the public Task ABI. The selected capability is immutable until the Task
* crosses its next sequence boundary. */
bool lardon3d_task_internal_set_capability_envelope(
Lardon3DTask *task,
const Lardon3DTaskCapabilityEnvelope *envelope
);
/* Confirms the universal fixed default after generic reconstruction. Optional
* kind-owned private hooks may then replace it using runtime/business context;
* this avoids guessing scientific eligibility from a resource estimate. */
bool lardon3d_task_internal_enable_known_capabilities(Lardon3DTask *task);
/* Queue and sequence_break are the only admission owners. A successful call
* records the one selection associated with reservation so task_start cannot
* install a contract from a different capability. */
bool lardon3d_task_internal_reserve_available(
Lardon3DTask *task,
Lardon3DResourceGovernor *governor,
Lardon3DResourceDecision *decision,
Lardon3DResourceReservation **reservation
);
bool lardon3d_task_internal_record_sequence(
Lardon3DTask *task,
uint64_t wall_time_ns,
size_t items_completed
);
bool lardon3d_task_internal_record_sequence_execution(
Lardon3DTask *task,
uint64_t wall_time_ns,
size_t items_completed,
Lardon3DResourceBackend actual_backend,
const char *backend_reason
);
bool lardon3d_task_internal_record_sequence_execution_metrics(
Lardon3DTask *task,
uint64_t wall_time_ns,
size_t items_completed,
Lardon3DResourceBackend actual_backend,
const char *backend_reason,
const Lardon3DResourceExecutionMetrics *metrics
);
/* Matcher calls this only after the exact candidate pair is durable. Task
* owns a bounded current-run high-water mark so an in-process retry cannot
* count the same ordered pair twice; restart intentionally reconstructs no
* operational telemetry. The operation does not create throughput feedback. */
bool lardon3d_task_internal_record_fallback_item(
Lardon3DTask *task,
uint64_t candidate_pair_id,
Lardon3DResourceFallbackItemCause cause
);
/* Copies the immutable operational selection installed for the executing
* sequence. Task retains ownership; callbacks use the copy only to honor
* private dimensions (currently Matcher inflight) absent from the stable
* public execution-contract ABI. Observation never changes admission. */
bool lardon3d_task_internal_execution_selection(
const Lardon3DTask *task,
Lardon3DResourceCapabilitySelection *selection
);
#ifdef LARDON3D_TASK_TESTING
/* Deterministically exercises the post-reserve association failure that is
* otherwise unreachable without corrupting private Task state. */
bool lardon3d_task_internal_test_force_sequence_association_mismatch(
Lardon3DTask *task
);
bool lardon3d_task_internal_test_has_reservation_ownership(
Lardon3DTask *task
);
unsigned int lardon3d_task_internal_test_association_failure_releases(
Lardon3DTask *task
);
#endif
#ifdef __cplusplus
}
#endif
#endif

View file

@ -4,6 +4,22 @@
#include <lardon3d/task_kind_registry.h>
#include "task_internal.h"
/* Production Matcher provides this private post-reconstruction hook. Minimal
* Registry-only targets intentionally omit it; weak absence preserves the
* universal fixed-envelope default without adding a public descriptor field. */
#if defined(__GNUC__) || defined(__clang__)
extern bool lardon3d_matcher_task_internal_configure_restored(
Lardon3DTask *task,
void *userdata
) __attribute__((weak));
extern bool lardon3d_acquisition_campaign_task_internal_configure_restored(
Lardon3DTask *task,
void *userdata
) __attribute__((weak));
#endif
enum {
CANDIDATE_LEGACY_FIXED_BYTES = 128 * 1024,
CANDIDATE_CURRENT_FIXED_BYTES = 256 * 1024,
@ -37,6 +53,8 @@ normalize_known_legacy_estimate(const char *kind,
{
Lardon3DResourceEstimate current = {0};
Lardon3DResourceEstimate historical = {0};
Lardon3DResourceEstimate oldest = {0};
bool has_oldest = false;
if (strcmp(kind, "candidate_pair.generate") == 0) {
current = (Lardon3DResourceEstimate) {
.memory_fixed_bytes = CANDIDATE_CURRENT_FIXED_BYTES,
@ -56,16 +74,33 @@ normalize_known_legacy_estimate(const char *kind,
.gpu_memory_fixed_bytes = gpu ? MATCHER_GPU_FIXED_BYTES : 0,
.memory_bytes_per_item = MATCHER_CURRENT_PER_ITEM_BYTES,
.minimum_batch_size = 1,
.maximum_batch_size = 8,
.desired_cpu_threads = gpu ? 1U : 8U,
.maximum_batch_size = 12,
.desired_cpu_threads = gpu ? 1U : 12U,
.desired_gpu_slots = gpu ? 1U : 0U,
.desired_io_slots = 1,
.task_class = LARDON3D_RESOURCE_TASK_CPU,
};
historical = current;
historical.memory_fixed_bytes = MATCHER_LEGACY_FIXED_BYTES;
historical.memory_bytes_per_item = 0;
historical.desired_cpu_threads = 12;
/* CPU8/GPU0 and CPU1/GPU1 are the immediately preceding durable
* operational forms. They are normalized only in memory: thread
* count is not Matcher scientific identity and no estimate-only
* checkpoint is published during recovery. */
historical.maximum_batch_size = 8;
historical.desired_cpu_threads = gpu ? 1U : 8U;
oldest = current;
oldest.memory_fixed_bytes = MATCHER_LEGACY_FIXED_BYTES;
oldest.memory_bytes_per_item = 0;
oldest.maximum_batch_size = 8;
oldest.desired_cpu_threads = 12;
has_oldest = true;
Lardon3DResourceEstimate automatic = current;
automatic.task_class = LARDON3D_RESOURCE_TASK_MIXED;
if (!gpu && estimate_equals(durable, &automatic)) {
/* MIXED is the truthful durable signature for new normal AUTO:
* execution may consume either its CPU or Vulkan capability. */
*effective = *durable;
return true;
}
} else if (strcmp(kind, "features.extract.sift") == 0
|| strcmp(kind, "features.extract.rootsift") == 0) {
current = (Lardon3DResourceEstimate) {
@ -87,7 +122,8 @@ normalize_known_legacy_estimate(const char *kind,
*effective = *durable;
return true;
}
if (!estimate_equals(durable, &historical)) {
if (!estimate_equals(durable, &historical)
&& (!has_oldest || !estimate_equals(durable, &oldest))) {
return false;
}
/* These exact signatures are historical operational policy, not scientific
@ -214,6 +250,28 @@ lardon3d_task_kind_registry_restore(
}
return LARDON3D_TASK_KIND_RESTORE_FAILED;
}
bool capabilities_configured =
lardon3d_task_internal_enable_known_capabilities(*task);
#if defined(__GNUC__) || defined(__clang__)
if (capabilities_configured && strcmp(kind, "matcher.run") == 0
&& lardon3d_matcher_task_internal_configure_restored) {
capabilities_configured =
lardon3d_matcher_task_internal_configure_restored(
*task, binding.userdata);
}
if (capabilities_configured
&& strcmp(kind, "acquisition_campaign.run") == 0
&& lardon3d_acquisition_campaign_task_internal_configure_restored) {
capabilities_configured =
lardon3d_acquisition_campaign_task_internal_configure_restored(
*task, binding.userdata);
}
#endif
if (!capabilities_configured) {
lardon3d_task_destroy(*task);
*task = NULL;
return LARDON3D_TASK_KIND_RESTORE_FAILED;
}
if (binding.finished_callback
&& !lardon3d_task_set_finished_callback(*task,
binding.finished_callback, binding.finished_userdata)) {

View file

@ -6,6 +6,8 @@
#include <lardon3d/task_queue.h>
#include "task_internal.h"
typedef struct TaskNode {
Lardon3DTask *task;
struct TaskNode *next_all;
@ -26,6 +28,7 @@ struct Lardon3DTaskQueue {
pthread_cond_t not_full;
pthread_t worker;
bool worker_started;
bool worker_ready;
bool stopping;
Lardon3DResourceGovernor *governor;
uint64_t next_id;
@ -94,16 +97,14 @@ select_admissible(
node = next;
continue;
}
Lardon3DResourceEstimate estimate;
Lardon3DResourceDecision decision;
Lardon3DResourceReservation *candidate = NULL;
bool evaluated = lardon3d_task_resource_estimate(node->task, &estimate)
&& lardon3d_resource_governor_reserve_available(
queue->governor,
&estimate,
&decision,
&candidate
);
bool evaluated = lardon3d_task_internal_reserve_available(
node->task,
queue->governor,
&decision,
&candidate
);
if (!evaluated) {
(void)lardon3d_task_reject(
node->task,
@ -160,6 +161,15 @@ static void *
queue_worker(void *context)
{
Lardon3DTaskQueue *queue = context;
/* Only this thread owns heavy Task callbacks. Applying the Governor mask
* here keeps the caller/main/TUI thread unconstrained; children created by
* OpenCV, Matcher, or the Vulkan driver inherit the bounded worker mask. */
(void)lardon3d_resource_governor_internal_apply_worker_affinity(
queue->governor);
(void)pthread_mutex_lock(&queue->mutex);
queue->worker_ready = true;
(void)pthread_cond_broadcast(&queue->not_empty);
(void)pthread_mutex_unlock(&queue->mutex);
/* Single worker only; execution is serialized by design. Queue preserves
* pending FIFO order with adaptive dispatch/backpressure; Governor decides
* admission.
@ -188,6 +198,12 @@ queue_worker(void *context)
queue->active = selected;
(void)pthread_mutex_unlock(&queue->mutex);
/* Policy may change between Tasks. Reapply and verify only on this
* worker; failure is recorded by the Governor and execution continues
* under the conservative compute-count admission without corrupting
* Queue ownership or durable scientific state. */
(void)lardon3d_resource_governor_internal_apply_worker_affinity(
queue->governor);
if (!lardon3d_task_start(selected, queue->governor, reservation)) {
(void)lardon3d_task_reject(
selected,
@ -261,6 +277,11 @@ lardon3d_task_queue_create(Lardon3DResourceGovernor *governor, size_t capacity)
return NULL;
}
queue->worker_started = true;
(void)pthread_mutex_lock(&queue->mutex);
while (!queue->worker_ready) {
(void)pthread_cond_wait(&queue->not_empty, &queue->mutex);
}
(void)pthread_mutex_unlock(&queue->mutex);
return queue;
}

View file

@ -12,6 +12,7 @@
#include <lardon3d/visual_index_task.h>
#include "visual_index_internal.h"
#include "task_internal.h"
enum { VISUAL_INDEX_TASK_CPU_THREADS = 12 };
@ -72,16 +73,16 @@ static bool run(Lardon3DTask *task, void *userdata) {
contract.cpu_threads == 0 || contract.cpu_threads > VISUAL_INDEX_TASK_CPU_THREADS) {
return lardon3d_task_fail(task, "Contrat Visual Index invalide.");
}
struct timespec begin;
struct timespec end;
clock_gettime(CLOCK_MONOTONIC, &begin);
struct timespec begin = {0};
struct timespec end = {0};
bool timing_known = clock_gettime(CLOCK_MONOTONIC, &begin) == 0;
uint64_t last = context->parameters.after_feature_set_id;
size_t indexed = 0;
Lardon3DVisualIndexResult result = lardon3d_visual_index_update_once_parallel(
context->project_path, context->database, context->parameters.visual_index_id,
lardon3d_task_id(task), context->parameters.after_feature_set_id, contract.batch_size,
contract.cpu_threads, &last, &indexed);
clock_gettime(CLOCK_MONOTONIC, &end);
timing_known = timing_known && clock_gettime(CLOCK_MONOTONIC, &end) == 0;
if (result == LARDON3D_VISUAL_INDEX_NO_CHANGE) {
return lardon3d_task_set_progress(task, 100, "Visual Index à jour.");
}
@ -90,8 +91,21 @@ static bool run(Lardon3DTask *task, void *userdata) {
return lardon3d_task_fail(task, "Mise à jour Visual Index impossible.");
}
context->parameters.after_feature_set_id = last;
lardon3d_resource_governor_record_batch(context->governor, LARDON3D_RESOURCE_TASK_CPU,
indexed, elapsed_ns(begin, end), 0);
size_t durable_indexed = result == LARDON3D_VISUAL_INDEX_OK ? indexed : 0;
uint64_t duration_ns = timing_known ? elapsed_ns(begin, end) : 0;
if (durable_indexed > 0 && duration_ns > 0) {
lardon3d_resource_governor_record_batch(
context->governor, LARDON3D_RESOURCE_TASK_CPU, durable_indexed,
duration_ns, 0);
}
/* Visual Index already consumes the immutable admitted CPU count. A
* segment whose directory publication is not durable remains visible for
* restart semantics but is zero operational work, so it cannot train the
* next sequence's 1/2/4/8/12 CPU trial. */
if (duration_ns > 0) {
(void)lardon3d_task_internal_record_sequence(
task, duration_ns, durable_indexed);
}
if (!lardon3d_task_set_progress(task, 99, "Segment Visual Index publié.") ||
lardon3d_project_checkpoint_visual_index_update_task(&state, task,
&context->parameters) !=

View file

@ -11,6 +11,8 @@
#include <iterator>
#include <vector>
#include "vulkan_process_startup.h"
namespace {
constexpr uint32_t kDescriptorBytes = 32;
@ -124,6 +126,11 @@ static bool benchmark_sustained(Lardon3DOrbVulkanBackend *backend) {
} // namespace
int main(int argc, char **argv) {
if (!lardon3d_vulkan_evidence_process_startup()) {
std::fprintf(stderr,
"MESA_SHADER_CACHE_DISABLE must be true for safe CPU affinity\n");
return 1;
}
Lardon3DOrbVulkanBackend *backend = lardon3d_orb_vulkan_backend_create();
if (!backend) {
return 1;

View file

@ -10,6 +10,8 @@
#include <opencv2/core.hpp>
#include <opencv2/features2d.hpp>
#include "vulkan_process_startup.h"
static uint32_t random_u32(uint32_t *state) {
uint32_t value = *state;
value ^= value << 13;
@ -165,6 +167,11 @@ static bool benchmark(uint32_t count_a, uint32_t count_b, bool rootsift) {
}
int main() {
if (!lardon3d_vulkan_evidence_process_startup()) {
std::fprintf(stderr,
"MESA_SHADER_CACHE_DISABLE must be true for safe CPU affinity\n");
return EXIT_FAILURE;
}
cv::setNumThreads(12);
std::printf("kind,count_a,count_b,cpu_ms,cold_ms,after_orb_ms,warm_ms,gpu_ms,"
"finalization_ms,gain_with_finalization,final_exact\n");

File diff suppressed because it is too large Load diff

View file

@ -7,8 +7,13 @@
extern "C" {
#include <lardon3d/acquisition_campaign_task.h>
#include <lardon3d/project_db.h>
#include "../src/task_internal.h"
}
extern "C" bool
lardon3d_acquisition_campaign_task_internal_configure_restored(
Lardon3DTask *, void *);
#define CHECK(x) \
do { \
if (!(x)) { \
@ -173,6 +178,45 @@ int main() {
CHECK(loaded_task.next_group_id == 1 && loaded_task.group_count == 2 &&
loaded_task.request_size == encoded.size() &&
std::memcmp(loaded.data(), encoded.data(), encoded.size()) == 0);
snapshot.estimate = Lardon3DResourceEstimate{
256 * 1024, 0, 64 * 1024, 0, 1, 1, 1, 0, 1,
LARDON3D_RESOURCE_TASK_IMPORT};
Lardon3DHardwareProfile profile{16, 4096, UINT64_MAX, false, 0, false,
false, 0, "test", ""};
Lardon3DResourcePolicy policy{};
policy.maximum_cpu_load_ratio = 1.0;
policy.maximum_io_pressure_avg10 = 100.0;
policy.io_slot_capacity = 1;
Lardon3DResourceGovernor *governor =
lardon3d_resource_governor_create(&profile, &policy);
CHECK(governor != nullptr);
Lardon3DTaskReconstructionContext valid_reconstruction{
"/tmp", database, governor, nullptr};
Lardon3DTaskKindBinding recovered{};
CHECK(lardon3d_acquisition_campaign_task_reconstruct(
&snapshot, &valid_reconstruction, &recovered));
Lardon3DTask *restored = lardon3d_task_restore_typed(
&snapshot, LARDON3D_ACQUISITION_CAMPAIGN_TASK_KIND,
LARDON3D_ACQUISITION_CAMPAIGN_TASK_KIND_VERSION, recovered.callback,
recovered.userdata, recovered.userdata_destroy);
CHECK(restored &&
lardon3d_acquisition_campaign_task_internal_configure_restored(
restored, recovered.userdata));
Lardon3DResourceDecision decision{};
Lardon3DResourceReservation *reservation = nullptr;
CHECK(lardon3d_task_internal_reserve_available(
restored, governor, &decision, &reservation));
Lardon3DResourceReservationInfo admitted{};
CHECK(reservation && lardon3d_resource_reservation_get_active(
governor, reservation, &admitted));
CHECK(admitted.memory_bytes > 320 * 1024);
Lardon3DTaskDurableSnapshot unchanged{};
CHECK(lardon3d_task_durable_snapshot(restored, &unchanged));
CHECK(unchanged.estimate.memory_fixed_bytes == 256 * 1024 &&
unchanged.estimate.memory_bytes_per_item == 64 * 1024);
CHECK(lardon3d_resource_governor_release(governor, reservation));
lardon3d_task_destroy(restored);
lardon3d_resource_governor_destroy(governor);
std::vector<char> overlong_project_path(LARDON3D_APP_STATE_PATH_CAPACITY + 1,
'x');
overlong_project_path.back() = '\0';

View file

@ -338,7 +338,10 @@ static bool run_test(void) {
&state, visual_index_id, &qopts, &multi_sequence_task_id) &&
wait_state(state.task_queue, multi_sequence_task_id, TASK_COMPLETED, &snap) &&
snap.progress == 100 &&
lardon3d_candidate_pair_task_test_started_participants() >= 6 &&
/* A fresh kind history begins at CPU1. This small fixture proves
* every durable item executes; synthetic throughput is not allowed
* to assume that CPU2 was already demonstrated beneficial. */
lardon3d_candidate_pair_task_test_started_participants() >= 1 &&
lardon3d_candidate_pair_task_test_computed_work_items() >= 6);
Lardon3DProjectDbTask multi_sequence_task;
CHECK(lardon3d_project_db_load_task(state.project_db, multi_sequence_task_id,

View file

@ -0,0 +1,37 @@
import os
import subprocess
import sys
def main() -> int:
if len(sys.argv) != 2:
return 2
environment = os.environ.copy()
environment["MESA_SHADER_CACHE_DISABLE"] = "false"
try:
result = subprocess.run(
[sys.argv[1]],
check=False,
env=environment,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
timeout=4,
)
except (OSError, subprocess.TimeoutExpired) as error:
print(f"unable to execute production boundary: {error}", file=sys.stderr)
return 1
expected = b"MESA_SHADER_CACHE_DISABLE must be true for safe CPU affinity\n"
if result.returncode != 1 or result.stdout or result.stderr != expected:
print(
"unexpected production boundary result: "
f"status={result.returncode} stdout={result.stdout!r} "
f"stderr={result.stderr!r}",
file=sys.stderr,
)
return 1
return 0
if __name__ == "__main__":
raise SystemExit(main())

View file

@ -1,3 +1,7 @@
#ifndef _GNU_SOURCE
#define _GNU_SOURCE
#endif
#include <dirent.h>
#include <errno.h>
#include <fcntl.h>
@ -24,6 +28,10 @@
#include <lardon3d/task_checkpoint.h>
#include <lardon3d/task_queue.h>
#include "../src/opencv_task_thread_control.h"
#include "../src/resource_governor_internal.h"
#include "../src/task_internal.h"
#define CHECK(x) \
do { \
if (!(x)) { \
@ -95,6 +103,196 @@ static bool write_uniform_pgm(const char *path) {
for (size_t y = 0; ok && y < 192; ++y) ok = fwrite(row, 1, sizeof(row), file) == sizeof(row);
return fclose(file) == 0 && ok;
}
static bool wait_state(Lardon3DTaskQueue *q, uint64_t id,
Lardon3DTaskState wanted,
Lardon3DTaskSnapshot *out);
static bool extracted_equal(const Lardon3DExtractedFeatures *left,
const Lardon3DExtractedFeatures *right) {
if (!left || !right || left->image_width != right->image_width ||
left->image_height != right->image_height ||
left->feature_count != right->feature_count ||
left->descriptor_bytes != right->descriptor_bytes ||
memcmp(&left->quality, &right->quality, sizeof(left->quality)) != 0 ||
(left->descriptor_bytes > 0 &&
memcmp(left->descriptors, right->descriptors,
left->descriptor_bytes) != 0)) {
return false;
}
for (uint32_t index = 0; index < left->feature_count; ++index) {
const Lardon3DFeatureKeypoint *a = &left->keypoints[index];
const Lardon3DFeatureKeypoint *b = &right->keypoints[index];
if (memcmp(&a->x, &b->x, sizeof(a->x)) != 0 ||
memcmp(&a->y, &b->y, sizeof(a->y)) != 0 ||
memcmp(&a->size, &b->size, sizeof(a->size)) != 0 ||
memcmp(&a->angle_degrees, &b->angle_degrees,
sizeof(a->angle_degrees)) != 0 ||
memcmp(&a->response, &b->response, sizeof(a->response)) != 0 ||
a->octave != b->octave) {
return false;
}
}
return true;
}
typedef enum {
TEST_EXTRACT_ORB = 0,
TEST_EXTRACT_SIFT = 1,
TEST_EXTRACT_ROOTSIFT = 2,
TEST_EXTRACT_COUNT = 3,
} TestExtractKind;
typedef struct {
const char *path;
unsigned int expected_threads;
TestExtractKind kind;
Lardon3DExtractedFeatures output;
} AdaptiveExtractWork;
static bool adaptive_extract_callback(Lardon3DTask *task, void *userdata) {
AdaptiveExtractWork *work = userdata;
Lardon3DOpenCvTaskThreadControl control;
if (!lardon3d_opencv_task_threads_begin(task, 12, &control) ||
lardon3d_feature_opencv_thread_count() != work->expected_threads) {
return false;
}
Lardon3DFeatureExtractResult result;
if (work->kind == TEST_EXTRACT_ORB) {
const Lardon3DFeatureExtractorParameters parameters = {512, 4, 10};
result = lardon3d_feature_extract_orb(work->path, &parameters,
&work->output);
} else {
Lardon3DSiftExtractorParameters parameters =
lardon3d_sift_precision_classic_v1(
work->kind == TEST_EXTRACT_ROOTSIFT);
parameters.max_features = 512;
result = lardon3d_feature_extract_sift(work->path, &parameters,
&work->output);
}
bool restored = lardon3d_opencv_task_threads_end(&control);
return result == LARDON3D_FEATURE_EXTRACT_OK && restored &&
lardon3d_task_set_progress(task, 100, "Extraction adaptative testée.");
}
static bool run_adaptive_output_equivalence(const char *path) {
cpu_set_t allowed_mask;
CPU_ZERO(&allowed_mask);
if (sched_getaffinity(0, sizeof(allowed_mask), &allowed_mask) != 0) {
return true;
}
unsigned int allowed_ids[LARDON3D_RESOURCE_CPU_MAX];
size_t allowed_count = 0;
for (unsigned int cpu = 0; cpu < CPU_SETSIZE &&
cpu < LARDON3D_RESOURCE_CPU_MAX; ++cpu) {
if (CPU_ISSET((size_t)cpu, &allowed_mask)) {
allowed_ids[allowed_count++] = cpu;
}
}
if (allowed_count == 0) return true;
const unsigned int requested[] = {1, 2, 4, 8, 12};
Lardon3DExtractedFeatures baseline[TEST_EXTRACT_COUNT] = {0};
bool have_baseline[TEST_EXTRACT_COUNT] = {false};
unsigned int previous_threads = lardon3d_feature_opencv_thread_count();
bool ok = true;
for (size_t request_index = 0;
request_index < sizeof(requested) / sizeof(requested[0]) && ok;
++request_index) {
unsigned int threads = requested[request_index];
if (threads > allowed_count) continue;
Lardon3DHardwareProfile profile = {
.logical_cpu_count = threads + 4,
.page_size_bytes = 4096,
.memory_total_bytes = UINT64_C(16) * 1024 * 1024 * 1024,
.cpu_architecture = "test",
};
Lardon3DResourcePolicy policy = {
.system_cpu_reserve = 4,
.maximum_cpu_load_ratio = 1.0,
.maximum_io_pressure_avg10 = 100.0,
.io_slot_capacity = 1,
};
Lardon3DResourceGovernor *governor =
lardon3d_resource_governor_create(&profile, &policy);
Lardon3DResourceCpuTopologyInput topology = {
.affinity_available = true,
.topology_available = false,
.allowed_cpu_count = threads,
};
for (unsigned int index = 0; index < threads; ++index) {
topology.allowed_cpu_ids[index] = allowed_ids[index];
}
Lardon3DTaskQueue *queue = NULL;
if (!governor ||
!lardon3d_resource_governor_internal_configure_cpu_topology(
governor, &topology) ||
!(queue = lardon3d_task_queue_create(governor, TEST_EXTRACT_COUNT))) {
lardon3d_task_queue_destroy(queue);
lardon3d_resource_governor_destroy(governor);
ok = false;
break;
}
AdaptiveExtractWork work[TEST_EXTRACT_COUNT] = {0};
uint64_t ids[TEST_EXTRACT_COUNT] = {0};
Lardon3DResourceEstimate estimate = {
.minimum_batch_size = 1,
.maximum_batch_size = 1,
.desired_cpu_threads = 12,
.task_class = LARDON3D_RESOURCE_TASK_CPU,
};
static const char *const task_kinds[TEST_EXTRACT_COUNT] = {
"features.extract",
"features.extract.sift",
"features.extract.rootsift",
};
for (size_t kind = 0; kind < TEST_EXTRACT_COUNT && ok; ++kind) {
work[kind] = (AdaptiveExtractWork) {
.path = path,
.expected_threads = threads,
.kind = (TestExtractKind)kind,
};
Lardon3DTask *task = lardon3d_task_create_typed(
"Extraction adaptative", &estimate, task_kinds[kind], 1,
adaptive_extract_callback, &work[kind], NULL);
Lardon3DTaskCapabilityEnvelope envelope = {
.count = 1,
.capabilities = {{
.estimate = estimate,
.backend = LARDON3D_RESOURCE_BACKEND_FIXED,
.inflight_limit = 1,
}},
};
envelope.capabilities[0].estimate.desired_cpu_threads = threads;
/* Scientific equality is tested at exact admitted counts independently
* of feedback timing; Governor progression/deadband has its own fully
* deterministic rate-injection regression. */
ok = task && lardon3d_task_internal_set_capability_envelope(
task, &envelope) && lardon3d_task_queue_add(queue, task, &ids[kind]);
if (!ok && task) lardon3d_task_destroy(task);
}
Lardon3DTaskSnapshot snapshot;
for (size_t kind = 0; kind < TEST_EXTRACT_COUNT && ok; ++kind) {
ok = wait_state(queue, ids[kind], TASK_COMPLETED, &snapshot);
if (!ok) break;
if (!have_baseline[kind]) {
baseline[kind] = work[kind].output;
work[kind].output = (Lardon3DExtractedFeatures) {0};
have_baseline[kind] = true;
} else {
ok = extracted_equal(&baseline[kind], &work[kind].output);
}
}
for (size_t kind = 0; kind < TEST_EXTRACT_COUNT; ++kind) {
lardon3d_extracted_features_destroy(&work[kind].output);
}
lardon3d_task_queue_destroy(queue);
lardon3d_resource_governor_destroy(governor);
}
for (size_t kind = 0; kind < TEST_EXTRACT_COUNT; ++kind) {
lardon3d_extracted_features_destroy(&baseline[kind]);
}
return lardon3d_feature_opencv_configure_threads(previous_threads) && ok;
}
static bool runtime(Lardon3DAppState *s) {
s->hardware_profile = (Lardon3DHardwareProfile){.logical_cpu_count = 64,
.page_size_bytes = 4096,
@ -110,7 +308,10 @@ static bool wait_state(Lardon3DTaskQueue *q, uint64_t id, Lardon3DTaskState want
Lardon3DTaskSnapshot *out) {
struct timespec deadline;
if (clock_gettime(CLOCK_MONOTONIC, &deadline) != 0) return false;
deadline.tv_sec += 5;
/* The equality fixture deliberately executes ORB/SIFT/RootSIFT at five CPU
* contracts. Instrumented OpenCV is slower than the normal build; keep the
* wait bounded without mistaking sanitizer overhead for scientific drift. */
deadline.tv_sec += 30;
for (;;) {
if (lardon3d_task_queue_get(q, id, out) && out->state == wanted) {
return true;
@ -124,6 +325,16 @@ static bool wait_state(Lardon3DTaskQueue *q, uint64_t id, Lardon3DTaskState want
}
}
static bool last_sequence_is_no_work(Lardon3DAppState *state, const char *task_kind) {
Lardon3DResourceSequenceDiagnostic diagnostic;
return lardon3d_resource_governor_internal_last_diagnostic(
state->resource_governor, task_kind, 1, &diagnostic) &&
diagnostic.items_completed == 0 &&
diagnostic.durable_items_per_second_milli == 0 &&
diagnostic.cpu_threads == 1 &&
strcmp(diagnostic.reason, "throughput-no-work") == 0;
}
typedef struct {
Lardon3DAppState *state;
uint64_t orb_feature_set_id;
@ -146,9 +357,11 @@ static bool run_test(void) {
CHECK(mkdtemp(root) && setenv("LARDON3D_PROJECTS_ROOT", root, 1) == 0);
char source[PATH_MAX];
CHECK(join_path(source, root, "source.pgm") && write_pgm(source));
CHECK(run_adaptive_output_equivalence(source));
Lardon3DAppState state;
lardon3d_app_state_init(&state);
CHECK(runtime(&state) && lardon3d_project_create(&state, "Features"));
CHECK(lardon3d_feature_opencv_configure_threads(3));
Lardon3DProjectDbScanSet scanset;
CHECK(lardon3d_image_catalog_create_scanset(&state, "A", &scanset));
Lardon3DProjectDbImage image;
@ -164,6 +377,7 @@ static bool run_test(void) {
CHECK(wait_state(state.task_queue, task_id, TASK_PAUSED, &snapshot));
lardon3d_task_queue_destroy(state.task_queue);
state.task_queue = NULL;
CHECK(lardon3d_feature_opencv_thread_count() == 3);
lardon3d_project_close(&state);
lardon3d_resource_governor_destroy(state.resource_governor);
state.resource_governor = NULL;
@ -171,10 +385,12 @@ static bool run_test(void) {
unsetenv("LARDON3D_TEST_FEATURE_SKIP_FINISHED_CHECKPOINT") == 0);
lardon3d_app_state_init(&state);
CHECK(runtime(&state) && lardon3d_project_open(&state, "Features"));
CHECK(lardon3d_feature_opencv_configure_threads(3));
Lardon3DProjectRecoverySummary summary;
CHECK(lardon3d_project_last_recovery_summary(&state, &summary) && summary.resumed == 1);
CHECK(wait_state(state.task_queue, task_id, TASK_COMPLETED, &snapshot) &&
snapshot.progress == 100);
CHECK(lardon3d_feature_opencv_thread_count() == 3);
unsigned char fp[32];
lardon3d_feature_extractor_parameter_fingerprint(&parameters, fp);
Lardon3DProjectDbFeatureSet set;
@ -198,6 +414,60 @@ static bool run_test(void) {
LARDON3D_FEATURE_STORE_OK);
lardon3d_feature_reader_close(reader);
uint64_t duplicate_orb_task = 0;
CHECK(lardon3d_project_enqueue_feature_extract(
&state, image.image_id, &parameters, &duplicate_orb_task) &&
wait_state(state.task_queue, duplicate_orb_task, TASK_COMPLETED, &snapshot) &&
last_sequence_is_no_work(&state, LARDON3D_FEATURE_EXTRACT_TASK_KIND));
/* The asset and DB row are visible, but forced directory-sync uncertainty
* means this extraction is not durable throughput evidence. */
Lardon3DFeatureExtractorParameters nondurable_orb_parameters = {508, 4, 10};
uint64_t nondurable_orb_task = 0;
CHECK(setenv("LARDON3D_TEST_FEATURE_FAIL_DIRECTORY_SYNC", "1", 1) == 0 &&
lardon3d_project_enqueue_feature_extract(
&state, image.image_id, &nondurable_orb_parameters,
&nondurable_orb_task) &&
wait_state(state.task_queue, nondurable_orb_task, TASK_COMPLETED, &snapshot) &&
last_sequence_is_no_work(&state, LARDON3D_FEATURE_EXTRACT_TASK_KIND) &&
unsetenv("LARDON3D_TEST_FEATURE_FAIL_DIRECTORY_SYNC") == 0);
uint64_t post_no_work_orb_task = 0;
CHECK(lardon3d_project_enqueue_feature_extract(
&state, image.image_id, &parameters, &post_no_work_orb_task) &&
wait_state(state.task_queue, post_no_work_orb_task, TASK_COMPLETED,
&snapshot) &&
last_sequence_is_no_work(&state, LARDON3D_FEATURE_EXTRACT_TASK_KIND));
/* Configuration failure is injected after OpenCV mutates its process-wide
* pool. Feature, SIFT, and RootSIFT must each restore the captured value. */
CHECK(lardon3d_feature_opencv_configure_threads(3));
CHECK(setenv("LARDON3D_TEST_OPENCV_CONFIGURE_FAILURE_THREADS", "1", 1) == 0);
Lardon3DFeatureExtractorParameters failed_orb_parameters = {509, 4, 10};
uint64_t failed_orb_task = 0;
CHECK(lardon3d_project_enqueue_feature_extract(
&state, image.image_id, &failed_orb_parameters, &failed_orb_task) &&
wait_state(state.task_queue, failed_orb_task, TASK_FAILED, &snapshot) &&
lardon3d_feature_opencv_thread_count() == 3);
Lardon3DSiftExtractorParameters failed_sift_parameters =
lardon3d_sift_precision_classic_v1(false);
failed_sift_parameters.max_features = 511;
uint64_t failed_sift_task = 0;
CHECK(lardon3d_project_enqueue_sift_extract(
&state, image.image_id, &failed_sift_parameters,
&failed_sift_task) &&
wait_state(state.task_queue, failed_sift_task, TASK_FAILED, &snapshot) &&
lardon3d_feature_opencv_thread_count() == 3);
failed_sift_parameters.rootsift = true;
failed_sift_parameters.max_features = 510;
uint64_t failed_rootsift_task = 0;
CHECK(lardon3d_project_enqueue_sift_extract(
&state, image.image_id, &failed_sift_parameters,
&failed_rootsift_task) &&
wait_state(state.task_queue, failed_rootsift_task, TASK_FAILED, &snapshot) &&
lardon3d_feature_opencv_thread_count() == 3 &&
unsetenv("LARDON3D_TEST_OPENCV_CONFIGURE_FAILURE_THREADS") == 0);
Lardon3DSiftExtractorParameters sift_parameters = lardon3d_sift_precision_classic_v1(false);
sift_parameters.max_features = 512;
uint64_t sift_task_id = 0, concurrent_identical_sift_task = 0;
@ -207,7 +477,8 @@ static bool run_test(void) {
&concurrent_identical_sift_task));
CHECK(sift_task_id != concurrent_identical_sift_task &&
wait_state(state.task_queue, sift_task_id, TASK_COMPLETED, &snapshot) &&
wait_state(state.task_queue, concurrent_identical_sift_task, TASK_COMPLETED, &snapshot));
wait_state(state.task_queue, concurrent_identical_sift_task, TASK_COMPLETED, &snapshot) &&
last_sequence_is_no_work(&state, LARDON3D_SIFT_EXTRACT_TASK_KIND));
unsigned char sift_fingerprint[32];
lardon3d_sift_extractor_parameter_fingerprint(&sift_parameters, sift_fingerprint);
unsigned char same_sift_fingerprint[32], changed_sift_fingerprint[32];
@ -281,6 +552,31 @@ static bool run_test(void) {
rootsift_fingerprint, &rootsift_set) ==
LARDON3D_PROJECT_DB_OK &&
rootsift_set.feature_set_id != sift_set.feature_set_id);
uint64_t duplicate_rootsift_task = 0;
CHECK(lardon3d_project_enqueue_sift_extract(
&state, image.image_id, &rootsift_parameters,
&duplicate_rootsift_task) &&
wait_state(state.task_queue, duplicate_rootsift_task, TASK_COMPLETED, &snapshot) &&
last_sequence_is_no_work(&state, LARDON3D_ROOTSIFT_EXTRACT_TASK_KIND));
Lardon3DSiftExtractorParameters nondurable_sift_parameters = sift_parameters;
nondurable_sift_parameters.max_features = 508;
uint64_t nondurable_sift_task = 0;
CHECK(setenv("LARDON3D_TEST_FEATURE_FAIL_DIRECTORY_SYNC", "1", 1) == 0 &&
lardon3d_project_enqueue_sift_extract(
&state, image.image_id, &nondurable_sift_parameters,
&nondurable_sift_task) &&
wait_state(state.task_queue, nondurable_sift_task, TASK_COMPLETED, &snapshot) &&
last_sequence_is_no_work(&state, LARDON3D_SIFT_EXTRACT_TASK_KIND));
nondurable_sift_parameters.rootsift = true;
nondurable_sift_parameters.max_features = 509;
uint64_t nondurable_rootsift_task = 0;
CHECK(lardon3d_project_enqueue_sift_extract(
&state, image.image_id, &nondurable_sift_parameters,
&nondurable_rootsift_task) &&
wait_state(state.task_queue, nondurable_rootsift_task, TASK_COMPLETED, &snapshot) &&
last_sequence_is_no_work(&state, LARDON3D_ROOTSIFT_EXTRACT_TASK_KIND) &&
unsetenv("LARDON3D_TEST_FEATURE_FAIL_DIRECTORY_SYNC") == 0);
/* SIFT and RootSIFT CPU1 checkpoints are exact historical operational
* signatures. Recovery uses CPU12 in memory without publishing an
* estimate-only checkpoint; a neighboring CPU2 shape is corruption. */
@ -507,6 +803,26 @@ static bool run_test(void) {
lardon3d_feature_reader_close(reader);
Lardon3DVisualIndexConfiguration index_configuration = {1, 256, 128};
Lardon3DVisualIndexConfiguration nondurable_index_configuration = {1, 255, 128};
uint64_t nondurable_visual_index_id = 0;
uint64_t nondurable_visual_task_id = 0;
CHECK(lardon3d_visual_index_create(
state.project_db, &set, &nondurable_index_configuration,
&nondurable_visual_index_id) == LARDON3D_VISUAL_INDEX_OK);
CHECK(setenv("LARDON3D_TEST_VISUAL_INDEX_FAIL_DIR_FSYNC", "1", 1) == 0 &&
setenv("LARDON3D_TEST_VISUAL_INDEX_PAUSE_AFTER_SEGMENT", "1", 1) == 0);
CHECK(lardon3d_project_enqueue_visual_index_update(
&state, nondurable_visual_index_id, &nondurable_visual_task_id));
CHECK(wait_state(state.task_queue, nondurable_visual_task_id, TASK_PAUSED,
&snapshot));
CHECK(last_sequence_is_no_work(
&state, LARDON3D_VISUAL_INDEX_UPDATE_TASK_KIND));
CHECK(unsetenv("LARDON3D_TEST_VISUAL_INDEX_FAIL_DIR_FSYNC") == 0 &&
unsetenv("LARDON3D_TEST_VISUAL_INDEX_PAUSE_AFTER_SEGMENT") == 0 &&
lardon3d_task_queue_resume(
state.task_queue, nondurable_visual_task_id) &&
wait_state(state.task_queue, nondurable_visual_task_id, TASK_COMPLETED,
&snapshot));
uint64_t visual_index_id = 0;
CHECK(lardon3d_visual_index_create(state.project_db, &set, &index_configuration,
&visual_index_id) == LARDON3D_VISUAL_INDEX_OK);

View file

@ -1,10 +1,17 @@
#include <errno.h>
#include <fcntl.h>
#include <limits.h>
#include <stdbool.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/stat.h>
#include <unistd.h>
#include <lardon3d/hardware_profile.h>
#include "../src/hardware_profile_internal.h"
#define CHECK(condition) \
do { \
if (!(condition)) { \
@ -13,6 +20,138 @@
} \
} while (0)
#define GIBIBYTES(value) ((uint64_t)(value) * 1024 * 1024 * 1024)
static bool
write_text(const char *path, const char *text)
{
int descriptor = open(path, O_WRONLY | O_CREAT | O_TRUNC | O_CLOEXEC,
0600);
if (descriptor < 0) return false;
size_t length = strlen(text);
ssize_t written = write(descriptor, text, length);
return close(descriptor) == 0 && written == (ssize_t)length;
}
static bool
read_exact_text(const char *path, const char *expected)
{
char buffer[64];
int descriptor = open(path, O_RDONLY | O_CLOEXEC | O_NOFOLLOW);
if (descriptor < 0) return false;
ssize_t count = read(descriptor, buffer, sizeof(buffer));
bool eof = count >= 0 && count < (ssize_t)sizeof(buffer)
&& read(descriptor, buffer + count, 1) == 0;
bool equal = eof && (size_t)count == strlen(expected)
&& memcmp(buffer, expected, (size_t)count) == 0;
return close(descriptor) == 0 && equal;
}
static bool
card_path(char path[PATH_MAX], const char *root, unsigned int index,
const char *name)
{
int written = snprintf(path, PATH_MAX, "%s/card%u/device/%s", root,
index, name);
return written > 0 && written < PATH_MAX;
}
static bool
create_card(const char *root, unsigned int index, const char *vram,
const char *gtt)
{
char card[PATH_MAX];
char device[PATH_MAX];
int card_written = snprintf(card, sizeof(card), "%s/card%u", root, index);
int device_written = snprintf(device, sizeof(device), "%s/device", card);
if (card_written <= 0 || (size_t)card_written >= sizeof(card)
|| device_written <= 0 || (size_t)device_written >= sizeof(device)
|| mkdir(card, 0700) != 0 || mkdir(device, 0700) != 0) {
return false;
}
char path[PATH_MAX];
return card_path(path, root, index, "vendor")
&& write_text(path, "0x1002\n")
&& card_path(path, root, index, "mem_info_vram_total")
&& write_text(path, vram)
&& (!gtt || (card_path(path, root, index, "mem_info_gtt_total")
&& write_text(path, gtt)));
}
static bool
remove_card(const char *root, unsigned int index)
{
char path[PATH_MAX];
const char *files[] = {
"vendor", "mem_info_vram_total", "mem_info_gtt_total",
};
for (size_t file = 0; file < sizeof(files) / sizeof(files[0]); ++file) {
if (!card_path(path, root, index, files[file])) return false;
if (unlink(path) != 0 && errno != ENOENT) return false;
}
int written = snprintf(path, sizeof(path), "%s/card%u/device", root,
index);
if (written <= 0 || (size_t)written >= sizeof(path) || rmdir(path) != 0)
return false;
written = snprintf(path, sizeof(path), "%s/card%u", root, index);
return written > 0 && (size_t)written < sizeof(path) && rmdir(path) == 0;
}
static bool
run_fake_gpu_test(void)
{
char root[] = "/tmp/lardon3d-hardware-profile-XXXXXX";
CHECK(mkdtemp(root));
Lardon3DHardwareProfile profile = {
.memory_total_bytes = GIBIBYTES(16),
};
/* Exact current-host evidence: the small 512 MiB aperture and system-scale
* GTT are UMA, while the payload capacity remains observable. */
CHECK(create_card(root, 1, "536870912\n", "7986020352\n"));
lardon3d_hardware_profile_detect_gpu_at_root(&profile, root);
CHECK(profile.gpu_available && profile.gpu_drm_card_index == 1
&& profile.gpu_memory_known
&& profile.gpu_memory_total_bytes == UINT64_C(536870912)
&& profile.gpu_uses_shared_memory);
CHECK(remove_card(root, 1));
CHECK(create_card(root, 0, "8589934592\n", "8589934592\n"));
lardon3d_hardware_profile_detect_gpu_at_root(&profile, root);
CHECK(profile.gpu_available && profile.gpu_memory_known
&& profile.gpu_memory_total_bytes == GIBIBYTES(8)
&& !profile.gpu_uses_shared_memory);
CHECK(remove_card(root, 0));
/* Missing GTT with a low aperture is intentionally conservative; malformed
* GTT cannot turn a large known VRAM device into UMA. */
CHECK(create_card(root, 2, "536870912\n", NULL));
lardon3d_hardware_profile_detect_gpu_at_root(&profile, root);
CHECK(profile.gpu_memory_known && profile.gpu_uses_shared_memory);
CHECK(remove_card(root, 2));
CHECK(create_card(root, 3, "8589934592\n", "-1\n"));
lardon3d_hardware_profile_detect_gpu_at_root(&profile, root);
CHECK(profile.gpu_memory_known && !profile.gpu_uses_shared_memory);
CHECK(remove_card(root, 3));
CHECK(create_card(root, 4, "+536870912\n", "7986020352\n"));
lardon3d_hardware_profile_detect_gpu_at_root(&profile, root);
CHECK(profile.gpu_available && !profile.gpu_memory_known
&& profile.gpu_uses_shared_memory);
CHECK(remove_card(root, 4));
profile.gpu_available = true;
profile.gpu_memory_known = true;
profile.gpu_uses_shared_memory = true;
profile.gpu_memory_total_bytes = 1;
lardon3d_hardware_profile_detect_gpu_at_root(&profile, root);
CHECK(!profile.gpu_available && !profile.gpu_memory_known
&& !profile.gpu_uses_shared_memory
&& profile.gpu_memory_total_bytes == 0 && profile.gpu_name[0] == '\0');
CHECK(rmdir(root) == 0);
return true;
}
static bool
run_test(void)
{
@ -34,10 +173,25 @@ run_test(void)
CHECK(profile.gpu_name[0]);
CHECK(profile.gpu_drm_card_index < 64);
}
/* Skip-safe current-host integration evidence. Exact sysfs values identify
* the validated 780M configuration without making its PCI device ID part
* of production policy or a portable test requirement. */
if (read_exact_text("/sys/class/drm/card1/device/vendor", "0x1002\n")
&& read_exact_text("/sys/class/drm/card1/device/device", "0x1900\n")
&& read_exact_text("/sys/class/drm/card1/device/mem_info_vram_total",
"536870912\n")
&& read_exact_text("/sys/class/drm/card1/device/mem_info_gtt_total",
"7986020352\n")) {
CHECK(profile.gpu_available && profile.gpu_drm_card_index == 1
&& profile.gpu_memory_known
&& profile.gpu_memory_total_bytes == UINT64_C(536870912)
&& profile.gpu_uses_shared_memory);
}
Lardon3DHardwareProfile second;
CHECK(lardon3d_hardware_profile_detect(&second, NULL, 0));
CHECK(second.logical_cpu_count == profile.logical_cpu_count);
CHECK(second.memory_total_bytes == profile.memory_total_bytes);
CHECK(run_fake_gpu_test());
return true;
}

View file

@ -11,6 +11,9 @@
#include <lardon3d/match_file.h>
#include <lardon3d/matcher.h>
#include <lardon3d/orb_vulkan_backend.h>
#include "../src/matcher_internal.h"
#define CHECK(condition) \
do { \
@ -495,6 +498,35 @@ static bool test_matcher_default_ratio(void) {
return true;
}
static bool test_matcher_private_path_bound(void) {
char project_path[4097];
memset(project_path, 'x', sizeof(project_path) - 1);
project_path[sizeof(project_path) - 1] = '\0';
Lardon3DProjectDbFeatureSet feature_set = {
.feature_set_id = 1,
.feature_count = 769,
.descriptor_type = LARDON3D_FEATURE_DESCRIPTOR_U8,
.descriptor_dimension = 32,
};
Lardon3DMatcherParams parameters = {
.kind = LARDON3D_MATCHER_ORB_BF,
.ratio_threshold = 0.75F,
};
Lardon3DOrbVulkanBackend *backend = lardon3d_orb_vulkan_backend_create();
Lardon3DMatcherPendingVulkanStage *pending =
(Lardon3DMatcherPendingVulkanStage *)(uintptr_t)1;
bool backend_fault = true;
CHECK(backend);
Lardon3DMatcherResult expected = lardon3d_orb_vulkan_should_use(769, 769)
? LARDON3D_MATCHER_IO_ERROR : LARDON3D_MATCHER_INVALID_ARGUMENT;
CHECK(lardon3d_matcher_begin_vulkan_stage(
project_path, &feature_set, &feature_set, &parameters, backend,
&pending, &backend_fault) == expected &&
pending == NULL && !backend_fault);
lardon3d_orb_vulkan_backend_destroy(backend);
return true;
}
static bool run_tests(void) {
CHECK(test_match_file_write_read());
CHECK(test_match_file_empty());
@ -510,6 +542,7 @@ static bool run_tests(void) {
CHECK(test_matcher_kind_string());
CHECK(test_matcher_fingerprint());
CHECK(test_matcher_default_ratio());
CHECK(test_matcher_private_path_bound());
return true;
}

View file

@ -179,6 +179,7 @@ static bool read_result_asset(const Fixture *fixture, const Lardon3DProjectDbFea
return read_result == LARDON3D_MATCH_FILE_OK;
}
#ifdef LARDON3D_MATCHER_E2E_VULKAN
static bool files_equal(const char *path_a, const char *path_b) {
int fd_a = open(path_a, O_RDONLY | O_CLOEXEC);
int fd_b = open(path_b, O_RDONLY | O_CLOEXEC);
@ -235,7 +236,6 @@ static uint32_t deterministic_random(uint32_t *state) {
return *state;
}
#ifdef LARDON3D_MATCHER_E2E_VULKAN
static bool test_cpu_vulkan_match_file_parity(void) {
const uint32_t feature_count = 768;
const size_t bytes = (size_t)feature_count * 32;

File diff suppressed because it is too large Load diff

View file

@ -1,9 +1,12 @@
#include <lardon3d/orb_vulkan_backend.h>
#include "../src/orb_vulkan_backend_internal.h"
#include <opencv2/core.hpp>
#include <opencv2/features2d.hpp>
#include <algorithm>
#include <atomic>
#include <cstdint>
#include <cstdio>
#include <cstdlib>
@ -115,24 +118,114 @@ static bool check_serialized_threads(Lardon3DOrbVulkanBackend *backend) {
std::vector<unsigned char> b = make_descriptors(1024, 0x22222222U);
std::vector<Lardon3DOrbTop2> expected_a = opencv_top2(a, 1024, b, 1024);
std::vector<Lardon3DOrbTop2> expected_b = opencv_top2(b, 1024, a, 1024);
std::vector<Lardon3DOrbTop2> actual_a(1024);
std::vector<Lardon3DOrbTop2> actual_b(1024);
Lardon3DOrbVulkanResult result_a = LARDON3D_ORB_VULKAN_FAILED;
Lardon3DOrbVulkanResult result_b = LARDON3D_ORB_VULKAN_FAILED;
std::thread thread_a([&] {
result_a = lardon3d_orb_vulkan_top2(backend, a.data(), 1024, b.data(), 1024,
actual_a.data(), actual_a.size());
});
std::thread thread_b([&] {
result_b = lardon3d_orb_vulkan_top2(backend, b.data(), 1024, a.data(), 1024,
actual_b.data(), actual_b.size());
});
thread_a.join();
thread_b.join();
return result_a == LARDON3D_ORB_VULKAN_OK &&
result_b == LARDON3D_ORB_VULKAN_OK &&
std::equal(expected_a.begin(), expected_a.end(), actual_a.begin(), equal_top2) &&
std::equal(expected_b.begin(), expected_b.end(), actual_b.begin(), equal_top2);
for (unsigned int iteration = 0; iteration < 32; ++iteration) {
std::vector<Lardon3DOrbTop2> actual_a(1024);
std::vector<Lardon3DOrbTop2> actual_b(1024);
Lardon3DOrbVulkanResult result_a = LARDON3D_ORB_VULKAN_FAILED;
Lardon3DOrbVulkanResult result_b = LARDON3D_ORB_VULKAN_FAILED;
std::atomic<unsigned int> ready{0};
std::atomic<bool> start{false};
auto await_start = [&] {
ready.fetch_add(1, std::memory_order_release);
while (!start.load(std::memory_order_acquire)) {
std::this_thread::yield();
}
};
std::thread thread_a([&] {
await_start();
result_a = lardon3d_orb_vulkan_top2(
backend, a.data(), 1024, b.data(), 1024, actual_a.data(),
actual_a.size());
});
std::thread thread_b([&] {
await_start();
result_b = lardon3d_orb_vulkan_top2(
backend, b.data(), 1024, a.data(), 1024, actual_b.data(),
actual_b.size());
});
while (ready.load(std::memory_order_acquire) != 2) {
std::this_thread::yield();
}
start.store(true, std::memory_order_release);
thread_a.join();
thread_b.join();
/* Both calls start from the same gate and repeat enough times to exercise
* the public transaction boundary without timing sleeps. Each result is
* compared with its own directional CPU reference, catching cross-call
* request/output substitution as well as spurious pending-slot failure. */
if (result_a != LARDON3D_ORB_VULKAN_OK ||
result_b != LARDON3D_ORB_VULKAN_OK ||
!std::equal(expected_a.begin(), expected_a.end(), actual_a.begin(),
equal_top2) ||
!std::equal(expected_b.begin(), expected_b.end(), actual_b.begin(),
equal_top2)) {
return false;
}
}
return true;
}
static bool check_driver_policy_case(const char *value, bool private_begin) {
if ((value && setenv("MESA_SHADER_CACHE_DISABLE", value, 1) != 0) ||
(!value && unsetenv("MESA_SHADER_CACHE_DISABLE") != 0)) {
return false;
}
Lardon3DOrbVulkanBackend *backend = lardon3d_orb_vulkan_backend_create();
if (!backend) return false;
unsigned char descriptor[kDescriptorBytes]{};
Lardon3DOrbTop2 output{7, 11, 13, 17, 19};
const Lardon3DOrbTop2 unchanged = output;
Lardon3DOrbVulkanInfo before{};
Lardon3DOrbVulkanInfo after{};
Lardon3DOrbVulkanRequest request{};
bool ok = lardon3d_orb_vulkan_backend_info(backend, &before) &&
!before.initialized && !before.available;
Lardon3DOrbVulkanResult result = private_begin
? lardon3d_orb_vulkan_internal_top2_begin(
backend, descriptor, 1, descriptor, 1, &request)
: lardon3d_orb_vulkan_top2(
backend, descriptor, 1, descriptor, 1, &output, 1);
ok = ok && result == LARDON3D_ORB_VULKAN_UNAVAILABLE &&
std::memcmp(&output, &unchanged, sizeof(output)) == 0 &&
lardon3d_orb_vulkan_backend_info(backend, &after) && after.initialized &&
!after.available;
const char *retained = std::getenv("MESA_SHADER_CACHE_DISABLE");
ok = ok && ((!value && !retained) ||
(value && retained && std::strcmp(value, retained) == 0));
lardon3d_orb_vulkan_backend_destroy(backend);
return ok;
}
static bool check_driver_policy_gate() {
/* Meson's safe test environment must not mask these late-boundary cases.
* Every case uses a fresh backend before any successful Vulkan request, so a
* failure proves rejection precedes Mesa rather than observing cached state. */
bool ok = check_driver_policy_case(nullptr, false) &&
check_driver_policy_case("false", true) &&
check_driver_policy_case("malformed", false);
return setenv("MESA_SHADER_CACHE_DISABLE", "true", 1) == 0 && ok;
}
static bool check_current_driver_policy_rejection() {
const char *value = std::getenv("MESA_SHADER_CACHE_DISABLE");
if (value && (std::strcmp(value, "true") == 0 ||
std::strcmp(value, "1") == 0)) {
return false;
}
Lardon3DOrbVulkanBackend *backend = lardon3d_orb_vulkan_backend_create();
if (!backend) return false;
unsigned char descriptor[kDescriptorBytes]{};
Lardon3DOrbTop2 output{7, 11, 13, 17, 19};
const Lardon3DOrbTop2 unchanged = output;
Lardon3DOrbVulkanResult result = lardon3d_orb_vulkan_top2(
backend, descriptor, 1, descriptor, 1, &output, 1);
Lardon3DOrbVulkanInfo info{};
bool ok = result == LARDON3D_ORB_VULKAN_UNAVAILABLE &&
std::memcmp(&output, &unchanged, sizeof(output)) == 0 &&
lardon3d_orb_vulkan_backend_info(backend, &info) &&
info.initialized && !info.available;
lardon3d_orb_vulkan_backend_destroy(backend);
return ok;
}
static bool check_cached_unavailable() {
@ -165,7 +258,299 @@ static bool check_invalid_inputs(Lardon3DOrbVulkanBackend *backend) {
0) == LARDON3D_ORB_VULKAN_INVALID_ARGUMENT;
}
static bool check_private_slot_contract(Lardon3DOrbVulkanBackend *backend) {
constexpr uint32_t count = 1024;
std::vector<unsigned char> a = make_descriptors(count, 0x30303030U);
std::vector<unsigned char> b = make_descriptors(count, 0x40404040U);
std::vector<Lardon3DOrbTop2> expected = opencv_top2(a, count, b, count);
std::vector<Lardon3DOrbTop2> actual(count);
Lardon3DOrbVulkanRequest invalid{};
if (lardon3d_orb_vulkan_internal_top2_finish(
backend, &invalid, actual.data(), actual.size()) !=
LARDON3D_ORB_VULKAN_INVALID_ARGUMENT) {
return false;
}
Lardon3DOrbVulkanRequest first{};
if (lardon3d_orb_vulkan_internal_top2_begin(
backend, a.data(), count, b.data(), count, &first) !=
LARDON3D_ORB_VULKAN_OK) {
return false;
}
/* Invalid finish output still consumes its exact request. A following
* submit proves that slot was not stranded by argument validation. */
if (lardon3d_orb_vulkan_internal_top2_finish(
backend, &first, nullptr, 0) !=
LARDON3D_ORB_VULKAN_INVALID_ARGUMENT ||
lardon3d_orb_vulkan_internal_top2_finish(
backend, &first, actual.data(), actual.size()) !=
LARDON3D_ORB_VULKAN_FAILED) {
return false;
}
Lardon3DOrbVulkanRequest second{};
if (
lardon3d_orb_vulkan_internal_top2_begin(
backend, a.data(), count, b.data(), count, &second) !=
LARDON3D_ORB_VULKAN_OK ||
lardon3d_orb_vulkan_internal_top2_finish(
backend, &second, actual.data(), actual.size()) !=
LARDON3D_ORB_VULKAN_OK) {
return false;
}
if (!std::equal(expected.begin(), expected.end(), actual.begin(), equal_top2)) {
return false;
}
Lardon3DOrbVulkanRequest discarded{};
Lardon3DOrbVulkanRequest reused{};
if (lardon3d_orb_vulkan_internal_top2_begin(
backend, a.data(), count, b.data(), count, &discarded) !=
LARDON3D_ORB_VULKAN_OK ||
lardon3d_orb_vulkan_internal_top2_discard(backend, &discarded) !=
LARDON3D_ORB_VULKAN_OK ||
lardon3d_orb_vulkan_internal_top2_begin(
backend, a.data(), count, b.data(), count, &reused) !=
LARDON3D_ORB_VULKAN_OK ||
reused.slot != discarded.slot || reused.generation == discarded.generation ||
lardon3d_orb_vulkan_internal_top2_finish(
backend, &reused, actual.data(), actual.size()) !=
LARDON3D_ORB_VULKAN_OK) {
return false;
}
return std::equal(expected.begin(), expected.end(), actual.begin(), equal_top2);
}
static bool check_two_request_identity(Lardon3DOrbVulkanBackend *backend) {
constexpr uint32_t count = 1024;
std::vector<unsigned char> a = make_descriptors(count, 0x71717171U);
std::vector<unsigned char> b = make_descriptors(count, 0x72727272U);
std::vector<unsigned char> c = make_descriptors(count, 0x73737373U);
std::vector<Lardon3DOrbTop2> expected_ab = opencv_top2(a, count, b, count);
std::vector<Lardon3DOrbTop2> expected_cb = opencv_top2(c, count, b, count);
std::vector<Lardon3DOrbTop2> actual_ab(count);
std::vector<Lardon3DOrbTop2> actual_cb(count);
Lardon3DOrbVulkanRequest request_ab{};
Lardon3DOrbVulkanRequest request_cb{};
Lardon3DOrbVulkanRequest third{};
if (!lardon3d_orb_vulkan_internal_begin_sequence(
backend, LARDON3D_ORB_VULKAN_MAX_INFLIGHT)) {
return false;
}
bool ok = lardon3d_orb_vulkan_internal_top2_begin(
backend, a.data(), count, b.data(), count, &request_ab) ==
LARDON3D_ORB_VULKAN_OK &&
lardon3d_orb_vulkan_internal_top2_begin(
backend, c.data(), count, b.data(), count, &request_cb) ==
LARDON3D_ORB_VULKAN_OK &&
request_ab.slot != request_cb.slot &&
lardon3d_orb_vulkan_internal_top2_begin(
backend, a.data(), count, c.data(), count, &third) ==
LARDON3D_ORB_VULKAN_FAILED;
Lardon3DOrbVulkanRequest mismatched = request_ab;
mismatched.generation = request_cb.generation + 1;
if (mismatched.generation == 0) mismatched.generation = 1;
if (ok) {
ok = lardon3d_orb_vulkan_internal_top2_finish(
backend, &mismatched, actual_ab.data(), actual_ab.size()) ==
LARDON3D_ORB_VULKAN_FAILED
&& lardon3d_orb_vulkan_internal_top2_finish(
backend, &request_cb, actual_cb.data(), actual_cb.size()) ==
LARDON3D_ORB_VULKAN_OK
&& lardon3d_orb_vulkan_internal_top2_finish(
backend, &request_ab, actual_ab.data(), actual_ab.size()) ==
LARDON3D_ORB_VULKAN_OK;
} else {
if (request_ab.generation != 0) {
(void)lardon3d_orb_vulkan_internal_top2_discard(backend, &request_ab);
}
if (request_cb.generation != 0) {
(void)lardon3d_orb_vulkan_internal_top2_discard(backend, &request_cb);
}
}
bool ended = lardon3d_orb_vulkan_internal_end_sequence(backend);
return ok && ended
&& std::equal(expected_ab.begin(), expected_ab.end(), actual_ab.begin(),
equal_top2)
&& std::equal(expected_cb.begin(), expected_cb.end(), actual_cb.begin(),
equal_top2);
}
static bool check_private_telemetry_lifecycle() {
Lardon3DOrbVulkanBackend *backend = lardon3d_orb_vulkan_backend_create();
if (!backend) return false;
constexpr uint32_t count = 1024;
std::vector<unsigned char> descriptors =
make_descriptors(count, 0x60606060U);
std::vector<Lardon3DOrbTop2> output(count);
Lardon3DOrbVulkanTelemetry telemetry{};
Lardon3DOrbVulkanRequest first{};
bool ok = lardon3d_orb_vulkan_internal_telemetry(backend, &telemetry)
&& telemetry.submits == 0 && telemetry.completions == 0
&& !telemetry.slot_pending && telemetry.pending_slots == 0;
ok = ok && lardon3d_orb_vulkan_internal_top2_begin(
backend, descriptors.data(), count, descriptors.data(), count, &first)
== LARDON3D_ORB_VULKAN_OK;
ok = ok && lardon3d_orb_vulkan_internal_telemetry(backend, &telemetry)
&& telemetry.submits == 1 && telemetry.completions == 0
&& telemetry.slot_pending && telemetry.pending_slots == 1
&& telemetry.serial > 0;
ok = ok && lardon3d_orb_vulkan_internal_top2_finish(
backend, &first, output.data(), output.size()) == LARDON3D_ORB_VULKAN_OK;
ok = ok && lardon3d_orb_vulkan_internal_telemetry(backend, &telemetry)
&& telemetry.submits == 1 && telemetry.completions == 1
&& !telemetry.slot_pending && telemetry.pending_slots == 0;
Lardon3DOrbVulkanRequest second{};
ok = ok && lardon3d_orb_vulkan_internal_top2_begin(
backend, descriptors.data(), count, descriptors.data(), count, &second)
== LARDON3D_ORB_VULKAN_OK;
ok = ok && lardon3d_orb_vulkan_internal_top2_discard(backend, &second)
== LARDON3D_ORB_VULKAN_OK;
ok = ok && lardon3d_orb_vulkan_internal_telemetry(backend, &telemetry)
&& telemetry.submits == 2 && telemetry.completions == 1
&& telemetry.discards == 1 && !telemetry.slot_pending
&& telemetry.pending_slots == 0;
/* Slot reuse after discard remains operational and metrics remain
* cumulative; no history allocation or public info-struct change is used. */
Lardon3DOrbVulkanRequest third{};
ok = ok && lardon3d_orb_vulkan_internal_top2_begin(
backend, descriptors.data(), count, descriptors.data(), count, &third)
== LARDON3D_ORB_VULKAN_OK;
ok = ok && lardon3d_orb_vulkan_internal_top2_finish(
backend, &third, output.data(), output.size()) == LARDON3D_ORB_VULKAN_OK;
ok = ok && lardon3d_orb_vulkan_internal_telemetry(backend, &telemetry)
&& telemetry.submits == 3 && telemetry.completions == 2
&& telemetry.discards == 1 && !telemetry.slot_pending;
lardon3d_orb_vulkan_backend_destroy(backend);
return ok;
}
#ifdef LARDON3D_ORB_VULKAN_TESTING
static bool check_capacity_lifecycle() {
Lardon3DOrbVulkanBackend *backend = lardon3d_orb_vulkan_backend_create();
if (!backend) return false;
std::vector<unsigned char> descriptors = make_descriptors(1024, 0x81818181U);
std::vector<Lardon3DOrbTop2> output(1024);
Lardon3DOrbVulkanInfo info{};
Lardon3DOrbVulkanTelemetry telemetry{};
bool ok = lardon3d_orb_vulkan_backend_info(backend, &info)
&& !info.initialized && info.permanent_payload_bytes == 0
&& lardon3d_orb_vulkan_top2(
backend, descriptors.data(), 1024, descriptors.data(), 1024,
output.data(), output.size()) == LARDON3D_ORB_VULKAN_OK
&& lardon3d_orb_vulkan_backend_info(backend, &info)
&& info.permanent_payload_bytes ==
LARDON3D_ORB_VULKAN_PER_SLOT_BYTES
&& lardon3d_orb_vulkan_internal_telemetry(backend, &telemetry)
&& telemetry.retained_capacity == 1
&& telemetry.retained_payload_bytes ==
LARDON3D_ORB_VULKAN_PER_SLOT_BYTES
&& !telemetry.sequence_capacity_active;
if (ok) {
ok = setenv("LARDON3D_TEST_VULKAN_SLOT_ALLOCATION_FAILURE", "1", 1) == 0
&& !lardon3d_orb_vulkan_internal_begin_sequence(backend, 2)
&& lardon3d_orb_vulkan_backend_info(backend, &info)
&& info.permanent_payload_bytes ==
LARDON3D_ORB_VULKAN_PER_SLOT_BYTES
&& lardon3d_orb_vulkan_internal_telemetry(backend, &telemetry)
&& telemetry.retained_capacity == 1
&& !telemetry.sequence_capacity_active;
}
bool environment_restored =
unsetenv("LARDON3D_TEST_VULKAN_SLOT_ALLOCATION_FAILURE") == 0;
if (ok) {
ok = lardon3d_orb_vulkan_internal_begin_sequence(backend, 2)
&& lardon3d_orb_vulkan_backend_info(backend, &info)
&& info.permanent_payload_bytes ==
2 * LARDON3D_ORB_VULKAN_PER_SLOT_BYTES
&& lardon3d_orb_vulkan_internal_telemetry(backend, &telemetry)
&& telemetry.retained_capacity == 2
&& telemetry.sequence_capacity_active
&& lardon3d_orb_vulkan_internal_end_sequence(backend)
&& lardon3d_orb_vulkan_backend_info(backend, &info)
&& info.permanent_payload_bytes ==
LARDON3D_ORB_VULKAN_PER_SLOT_BYTES;
}
Lardon3DOrbVulkanRequest pending{};
if (ok) {
ok = lardon3d_orb_vulkan_internal_begin_sequence(backend, 1)
&& lardon3d_orb_vulkan_internal_top2_begin(
backend, descriptors.data(), 1024, descriptors.data(), 1024,
&pending) == LARDON3D_ORB_VULKAN_OK
&& !lardon3d_orb_vulkan_internal_end_sequence(backend)
&& lardon3d_orb_vulkan_internal_top2_finish(
backend, &pending, output.data(), output.size()) ==
LARDON3D_ORB_VULKAN_OK
&& lardon3d_orb_vulkan_internal_end_sequence(backend);
}
lardon3d_orb_vulkan_backend_destroy(backend);
return environment_restored && ok;
}
static bool check_generation_saturation() {
Lardon3DOrbVulkanBackend *backend = lardon3d_orb_vulkan_backend_create();
if (!backend) return false;
std::vector<unsigned char> descriptors = make_descriptors(1024, 0x91919191U);
std::vector<Lardon3DOrbTop2> output(1024);
Lardon3DOrbVulkanRequest ancient{};
Lardon3DOrbVulkanRequest terminal{};
Lardon3DOrbVulkanRequest future{};
bool ok = lardon3d_orb_vulkan_internal_begin_sequence(backend, 2)
&& lardon3d_orb_vulkan_internal_top2_begin(
backend, descriptors.data(), 1024, descriptors.data(), 1024,
&ancient) == LARDON3D_ORB_VULKAN_OK
&& ancient.generation == 1
&& lardon3d_orb_vulkan_internal_top2_finish(
backend, &ancient, output.data(), output.size()) ==
LARDON3D_ORB_VULKAN_OK
&& lardon3d_orb_vulkan_internal_test_set_slot_generation(
backend, ancient.slot, UINT64_MAX - 1)
/* UINT64_MAX is the terminal valid request identity. The following
* begin must issue it exactly once; only the subsequent begin retires
* this slot, before any submission could wrap to generation one. */
&& lardon3d_orb_vulkan_internal_top2_begin(
backend, descriptors.data(), 1024, descriptors.data(), 1024,
&terminal) == LARDON3D_ORB_VULKAN_OK
&& terminal.slot == ancient.slot && terminal.generation == UINT64_MAX
&& lardon3d_orb_vulkan_internal_top2_finish(
backend, &terminal, output.data(), output.size()) ==
LARDON3D_ORB_VULKAN_OK
&& lardon3d_orb_vulkan_internal_top2_begin(
backend, descriptors.data(), 1024, descriptors.data(), 1024,
&future) == LARDON3D_ORB_VULKAN_OK
&& future.slot != ancient.slot && future.generation == 1
&& lardon3d_orb_vulkan_internal_top2_finish(
backend, &ancient, output.data(), output.size()) ==
LARDON3D_ORB_VULKAN_FAILED;
Lardon3DOrbVulkanTelemetry telemetry{};
if (ok) {
ok = lardon3d_orb_vulkan_internal_telemetry(backend, &telemetry)
&& telemetry.pending_slots == 1
&& lardon3d_orb_vulkan_internal_top2_finish(
backend, &future, output.data(), output.size()) ==
LARDON3D_ORB_VULKAN_OK
&& lardon3d_orb_vulkan_internal_end_sequence(backend);
}
Lardon3DOrbVulkanRequest after_boundary{};
if (ok) {
ok = lardon3d_orb_vulkan_internal_begin_sequence(backend, 1)
&& lardon3d_orb_vulkan_internal_top2_begin(
backend, descriptors.data(), 1024, descriptors.data(), 1024,
&after_boundary) == LARDON3D_ORB_VULKAN_OK
&& after_boundary.slot == future.slot
&& after_boundary.generation == 2
&& lardon3d_orb_vulkan_internal_top2_finish(
backend, &ancient, output.data(), output.size()) ==
LARDON3D_ORB_VULKAN_FAILED
&& lardon3d_orb_vulkan_internal_top2_finish(
backend, &after_boundary, output.data(), output.size()) ==
LARDON3D_ORB_VULKAN_OK
&& lardon3d_orb_vulkan_internal_end_sequence(backend);
}
lardon3d_orb_vulkan_backend_destroy(backend);
return ok;
}
static bool check_cached_device_failure() {
Lardon3DOrbVulkanBackend *backend = lardon3d_orb_vulkan_backend_create();
std::vector<unsigned char> descriptors = make_descriptors(1024, 0x20202020U);
@ -184,11 +569,92 @@ static bool check_cached_device_failure() {
return first == LARDON3D_ORB_VULKAN_FAILED &&
second == LARDON3D_ORB_VULKAN_UNAVAILABLE;
}
static bool check_discard_wait_failure() {
Lardon3DOrbVulkanBackend *backend = lardon3d_orb_vulkan_backend_create();
if (!backend) {
return false;
}
std::vector<unsigned char> descriptors = make_descriptors(1024, 0x50505050U);
Lardon3DOrbVulkanRequest request{};
bool ok = lardon3d_orb_vulkan_internal_top2_begin(
backend, descriptors.data(), 1024, descriptors.data(), 1024,
&request) ==
LARDON3D_ORB_VULKAN_OK;
if (ok && setenv("LARDON3D_TEST_VULKAN_WAIT_FAILURE", "1", 1) != 0) {
ok = false;
}
if (ok) {
ok = lardon3d_orb_vulkan_internal_top2_discard(backend, &request) ==
LARDON3D_ORB_VULKAN_FAILED;
}
unsetenv("LARDON3D_TEST_VULKAN_WAIT_FAILURE");
if (ok) {
ok = lardon3d_orb_vulkan_internal_top2_begin(
backend, descriptors.data(), 1024, descriptors.data(), 1024,
&request) ==
LARDON3D_ORB_VULKAN_UNAVAILABLE;
}
Lardon3DOrbVulkanTelemetry telemetry{};
if (ok) {
ok = lardon3d_orb_vulkan_internal_telemetry(backend, &telemetry)
&& telemetry.submits == 1 && telemetry.discards == 1
&& telemetry.failures == 1 && !telemetry.slot_pending;
}
lardon3d_orb_vulkan_backend_destroy(backend);
return ok;
}
static bool check_finish_failure(const char *failure_variable,
uint64_t expected_completions) {
Lardon3DOrbVulkanBackend *backend = lardon3d_orb_vulkan_backend_create();
if (!backend || !failure_variable) {
lardon3d_orb_vulkan_backend_destroy(backend);
return false;
}
std::vector<unsigned char> descriptors = make_descriptors(1024, 0x61616161U);
std::vector<Lardon3DOrbTop2> output(1024);
Lardon3DOrbVulkanRequest request{};
bool ok = lardon3d_orb_vulkan_internal_top2_begin(
backend, descriptors.data(), 1024, descriptors.data(), 1024,
&request) == LARDON3D_ORB_VULKAN_OK
&& setenv(failure_variable, "1", 1) == 0;
if (ok) {
ok = lardon3d_orb_vulkan_internal_top2_finish(
backend, &request, output.data(), output.size()) ==
LARDON3D_ORB_VULKAN_FAILED;
}
bool environment_restored = unsetenv(failure_variable) == 0;
if (ok) {
ok = lardon3d_orb_vulkan_internal_top2_begin(
backend, descriptors.data(), 1024, descriptors.data(), 1024,
&request) == LARDON3D_ORB_VULKAN_UNAVAILABLE;
}
Lardon3DOrbVulkanTelemetry telemetry{};
if (ok) {
ok = lardon3d_orb_vulkan_internal_telemetry(backend, &telemetry)
&& telemetry.submits == 1
&& telemetry.completions == expected_completions
&& telemetry.failures == 1 && telemetry.pending_slots == 0
&& !telemetry.slot_pending;
}
lardon3d_orb_vulkan_backend_destroy(backend);
return environment_restored && ok;
}
#endif
} // namespace
int main() {
int main(int argc, char **argv) {
if (argc == 2 && std::strcmp(argv[1], "--unsafe-policy-only") == 0) {
return check_current_driver_policy_rejection() ? 0 : 1;
}
if (argc != 1) return 2;
if (!check_driver_policy_gate()) {
std::fprintf(stderr, "Vulkan driver process-policy gate failed\n");
return 1;
}
Lardon3DOrbVulkanBackend *backend = lardon3d_orb_vulkan_backend_create();
if (!backend) {
return 1;
@ -202,6 +668,21 @@ int main() {
ok = check_case(backend, 64, 2, 0x88112233U) && ok;
ok = check_ties(backend) && ok;
ok = check_invalid_inputs(backend) && ok;
const bool private_slot_ok = check_private_slot_contract(backend);
if (!private_slot_ok) {
std::fprintf(stderr, "private Vulkan slot contract failed\n");
}
ok = private_slot_ok && ok;
const bool two_request_ok = check_two_request_identity(backend);
if (!two_request_ok) {
std::fprintf(stderr, "private Vulkan two-request identity failed\n");
}
ok = two_request_ok && ok;
const bool telemetry_ok = check_private_telemetry_lifecycle();
if (!telemetry_ok) {
std::fprintf(stderr, "private Vulkan telemetry lifecycle failed\n");
}
ok = telemetry_ok && ok;
ok = check_serialized_threads(backend) && ok;
ok = !lardon3d_orb_vulkan_should_use(256, 256) && ok;
ok = !lardon3d_orb_vulkan_should_use(512, 512) && ok;
@ -209,7 +690,9 @@ int main() {
ok = lardon3d_orb_vulkan_should_use(1024, 1024) && ok;
Lardon3DOrbVulkanInfo info{};
ok = lardon3d_orb_vulkan_backend_info(backend, &info) && info.available && ok;
ok = lardon3d_orb_vulkan_backend_info(backend, &info) && info.available &&
info.permanent_payload_bytes ==
LARDON3D_ORB_VULKAN_PER_SLOT_BYTES && ok;
if (info.available) {
std::printf("device=%s workgroup=%u payload=%llu init_ms=%.3f gpu_ms=%.3f\n",
info.device_name, info.workgroup_size,
@ -220,7 +703,34 @@ int main() {
lardon3d_orb_vulkan_backend_destroy(backend);
ok = check_cached_unavailable() && ok;
#ifdef LARDON3D_ORB_VULKAN_TESTING
const bool capacity_ok = check_capacity_lifecycle();
if (!capacity_ok) {
std::fprintf(stderr, "Vulkan sequence-capacity lifecycle failed\n");
}
ok = capacity_ok && ok;
const bool generation_ok = check_generation_saturation();
if (!generation_ok) {
std::fprintf(stderr, "Vulkan request generation saturation failed\n");
}
ok = generation_ok && ok;
ok = check_cached_device_failure() && ok;
const bool discard_wait_ok = check_discard_wait_failure();
if (!discard_wait_ok) {
std::fprintf(stderr, "Vulkan discard wait-failure contract failed\n");
}
ok = discard_wait_ok && ok;
const bool finish_wait_ok = check_finish_failure(
"LARDON3D_TEST_VULKAN_FINISH_WAIT_FAILURE", 0);
if (!finish_wait_ok) {
std::fprintf(stderr, "Vulkan finish wait-failure contract failed\n");
}
ok = finish_wait_ok && ok;
const bool readback_ok = check_finish_failure(
"LARDON3D_TEST_VULKAN_READBACK_FAILURE", 1);
if (!readback_ok) {
std::fprintf(stderr, "Vulkan readback-failure contract failed\n");
}
ok = readback_ok && ok;
#endif
return ok ? 0 : 1;
}

View file

@ -108,11 +108,13 @@ int main() {
size_t request_probe_size = 0;
CHECK(lardon3d_photo_quality_request_encode(&request, nullptr, 0, &request_probe_size));
cv::setNumThreads(3);
uint64_t task_id = 0;
CHECK(lardon3d_project_enqueue_photo_quality(&state, scanset.scanset_id, &request, &task_id));
Lardon3DTaskSnapshot terminal{};
CHECK(wait_terminal(state.task_queue, task_id, &terminal));
CHECK(terminal.state == TASK_COMPLETED && terminal.progress == 100);
CHECK(cv::getNumThreads() == 3);
Lardon3DProjectDbPhotoQualityResult paired{}, raw_only{};
CHECK(lardon3d_project_db_load_photo_quality_result(database, task_id, 1, &paired) ==
LARDON3D_PROJECT_DB_OK);

View file

@ -0,0 +1,495 @@
#include <cstdlib>
#include <iostream>
#include <string>
#include <unistd.h>
#include <vector>
#define main lardon3d_pre_sfm_real_execution_main
#include "pre_sfm_real_execution.cpp"
#undef main
#define CHECK(condition) \
do { \
if (!(condition)) { \
std::cerr << "failed line " << __LINE__ << ": " << #condition << '\n'; \
return EXIT_FAILURE; \
} \
} while (false)
static bool parse_case(std::initializer_list<const char *> arguments,
Options &options) {
std::vector<std::string> storage(arguments.begin(), arguments.end());
std::vector<char *> argv;
argv.reserve(storage.size());
for (std::string &argument : storage) argv.push_back(argument.data());
return parse_options(static_cast<int>(argv.size()), argv.data(), options);
}
static int invoke_case(std::initializer_list<const char *> arguments) {
std::vector<std::string> storage(arguments.begin(), arguments.end());
std::vector<char *> argv;
argv.reserve(storage.size());
for (std::string &argument : storage) argv.push_back(argument.data());
return lardon3d_pre_sfm_real_execution_main(
static_cast<int>(argv.size()), argv.data());
}
static bool capture_matcher_evidence(Runtime &runtime, std::string &output) {
FILE *capture = std::tmpfile();
if (!capture) return false;
const int saved_stdout = dup(STDOUT_FILENO);
if (saved_stdout < 0 || std::fflush(stdout) != 0 ||
dup2(fileno(capture), STDOUT_FILENO) < 0) {
if (saved_stdout >= 0) close(saved_stdout);
std::fclose(capture);
return false;
}
(void)end_matcher_evidence(runtime);
const bool flushed = std::fflush(stdout) == 0;
const bool restored = dup2(saved_stdout, STDOUT_FILENO) >= 0;
close(saved_stdout);
bool read_ok = flushed && restored && std::fseek(capture, 0, SEEK_SET) == 0;
char buffer[1024];
while (read_ok) {
const size_t count = std::fread(buffer, 1, sizeof(buffer), capture);
output.append(buffer, count);
if (count < sizeof(buffer)) {
read_ok = std::feof(capture) != 0 && std::ferror(capture) == 0;
break;
}
}
return std::fclose(capture) == 0 && read_ok;
}
static bool json_valid_and_submit_cpu(const std::string &line,
bool require_submit_cpu) {
sqlite3 *database = nullptr;
sqlite3_stmt *statement = nullptr;
bool ok = sqlite3_open(":memory:", &database) == SQLITE_OK &&
sqlite3_prepare_v2(
database,
"SELECT json_valid(?1), json_extract(?1, "
"'$.vulkan_submit_cpu_ns')",
-1, &statement, nullptr) == SQLITE_OK &&
sqlite3_bind_text(statement, 1, line.c_str(), -1,
SQLITE_TRANSIENT) == SQLITE_OK &&
sqlite3_step(statement) == SQLITE_ROW &&
sqlite3_column_int(statement, 0) == 1;
if (ok && require_submit_cpu) {
ok = sqlite3_column_type(statement, 1) == SQLITE_INTEGER &&
sqlite3_column_int64(statement, 1) == 123456789;
}
if (statement) sqlite3_finalize(statement);
if (database && sqlite3_close(database) != SQLITE_OK) ok = false;
return ok;
}
static bool matcher_evidence_aggregate_case() {
Lardon3DHardwareProfile profile{};
profile.logical_cpu_count = 16;
profile.page_size_bytes = 4096;
profile.memory_total_bytes = UINT64_C(16) * 1024 * 1024 * 1024;
profile.gpu_available = true;
profile.gpu_uses_shared_memory = true;
std::snprintf(profile.cpu_architecture, sizeof(profile.cpu_architecture),
"%s", "test");
Lardon3DResourcePolicy policy{};
if (!lardon3d_resource_policy_default(&profile, &policy)) return false;
policy.gpu_slot_capacity = 1;
Lardon3DResourceGovernor *governor =
lardon3d_resource_governor_create(&profile, &policy);
if (!governor) return false;
Lardon3DTaskCapabilityEnvelope envelope{};
envelope.count = 1;
envelope.capabilities[0].estimate.memory_bytes_per_item = 10 * 1024 * 1024;
envelope.capabilities[0].estimate.minimum_batch_size = 4;
envelope.capabilities[0].estimate.maximum_batch_size = 4;
envelope.capabilities[0].estimate.desired_cpu_threads = 1;
envelope.capabilities[0].estimate.desired_gpu_slots = 1;
envelope.capabilities[0].estimate.desired_io_slots = 1;
envelope.capabilities[0].estimate.task_class = LARDON3D_RESOURCE_TASK_GPU;
envelope.capabilities[0].backend = LARDON3D_RESOURCE_BACKEND_ORB_VULKAN;
envelope.capabilities[0].inflight_limit = 2;
envelope.capabilities[0].minimum_inflight_limit = 2;
envelope.capabilities[0].gpu_memory_bytes_per_inflight = 640 * 1024;
Lardon3DResourceSnapshot snapshot{};
if (clock_gettime(CLOCK_MONOTONIC, &snapshot.captured_at) != 0) {
lardon3d_resource_governor_destroy(governor);
return false;
}
snapshot.memory_available_bytes = UINT64_C(8) * 1024 * 1024 * 1024;
snapshot.swap_activity_known = true;
Lardon3DResourceCapabilitySelection selection{};
Lardon3DResourceReservation *reservation = nullptr;
const bool admitted =
lardon3d_resource_governor_internal_reserve_capability(
governor, &snapshot, LARDON3D_MATCHER_TASK_KIND,
LARDON3D_MATCHER_TASK_KIND_VERSION, &envelope, &selection,
&reservation) &&
reservation;
const bool released =
admitted && lardon3d_resource_governor_release(governor, reservation);
Lardon3DResourceExecutionMetrics metrics{};
metrics.vulkan_submits = 2;
metrics.vulkan_completions = 2;
metrics.vulkan_submit_cpu_ns = 123456789;
metrics.vulkan_fence_wait_ns = 11;
metrics.vulkan_readback_ns = 12;
metrics.vulkan_gpu_time_known = true;
metrics.vulkan_gpu_ns = 13;
metrics.vulkan_starvation_ns = 14;
metrics.matcher_cpu_ns = 15;
metrics.publication_ns = 16;
metrics.local_ineligible_fallback_items = 1;
const bool recorded = released &&
lardon3d_resource_governor_internal_record_fallback_items(
governor, LARDON3D_MATCHER_TASK_KIND,
LARDON3D_MATCHER_TASK_KIND_VERSION, &selection,
LARDON3D_RESOURCE_FALLBACK_ITEM_LOCAL_INELIGIBLE, 1) &&
lardon3d_resource_governor_internal_record_sequence_execution_metrics(
governor, LARDON3D_MATCHER_TASK_KIND,
LARDON3D_MATCHER_TASK_KIND_VERSION, &selection, 1000, 1,
LARDON3D_RESOURCE_BACKEND_MIXED,
"vulkan-and-ineligible-pair-cpu-fallback", &metrics);
Runtime runtime{};
runtime.state.resource_governor = governor;
runtime.state.hardware_profile = profile;
runtime.matcher_inflight_override = 2;
runtime.matcher_batch_override = 4;
begin_matcher_evidence(runtime);
std::string output;
const bool captured = recorded && capture_matcher_evidence(runtime, output);
lardon3d_resource_governor_destroy(governor);
if (!captured) {
std::cerr << "unable to construct/capture Matcher evidence fixture: "
<< "admitted=" << admitted << " released=" << released
<< " recorded=" << recorded << '\n';
return false;
}
bool saw_aggregate = false;
size_t offset = 0;
while (offset < output.size()) {
const size_t newline = output.find('\n', offset);
const size_t end = newline == std::string::npos ? output.size() : newline;
const std::string line = output.substr(offset, end - offset);
if (!line.empty()) {
const bool aggregate = line.find(
"\"record\":\"matcher_evidence_aggregate\"") !=
std::string::npos;
if (!json_valid_and_submit_cpu(line, aggregate)) {
std::cerr << "invalid Matcher evidence JSON: " << line << '\n';
return false;
}
saw_aggregate = saw_aggregate || aggregate;
}
if (newline == std::string::npos) break;
offset = newline + 1;
}
const bool exact_field =
output.find("\"vulkan_submit_cpu_ns\":123456789") !=
std::string::npos;
const bool affinity_fields =
output.find("\"runtime_thread_policy_active\":true") !=
std::string::npos &&
output.find("\"mesa_shader_cache_disabled\":true") !=
std::string::npos &&
output.find("\"runtime_thread_policy_reason\":"
"\"worker-self-affinity-plus-mesa-disk-cache-disabled\"") !=
std::string::npos &&
output.find("auxiliary_affinity_active") == std::string::npos;
const bool inflight_field =
output.find("\"matcher_inflight_override\":2") != std::string::npos;
const bool batch_field =
output.find("\"matcher_batch_override\":4") != std::string::npos;
const bool experiment_fields =
output.find("\"experiment_valid\":false") != std::string::npos &&
output.find("\"experiment_reason\":\"backend-telemetry-unavailable\"") !=
std::string::npos &&
output.find("\"local_ineligible_fallback_sequences\":1") !=
std::string::npos &&
output.find("\"local_ineligible_fallback_items\":1") !=
std::string::npos &&
output.find("\"backend_failure_fallback_items\":0") !=
std::string::npos &&
output.find("\"backend_other_fallback_items\":0") !=
std::string::npos &&
output.find(
"\"comparison_requires_equal_local_ineligible_fallback_items\":"
"true") != std::string::npos;
if (!saw_aggregate || !exact_field || !affinity_fields || !inflight_field ||
!batch_field || !experiment_fields)
std::cerr << "missing exact Matcher aggregate field: " << output << '\n';
return saw_aggregate && exact_field && affinity_fields && inflight_field &&
batch_field && experiment_fields;
}
static bool forced_matcher_experiment_validation_case() {
Runtime runtime{};
runtime.matcher_inflight_override = 1;
Lardon3DResourceSequenceAggregate aggregate{};
aggregate.admission_count = 2;
aggregate.sequence_count = 2;
aggregate.selected_backend_admissions[
LARDON3D_RESOURCE_BACKEND_ORB_VULKAN] = 2;
aggregate.actual_backend_sequences[
LARDON3D_RESOURCE_BACKEND_ORB_VULKAN] = 2;
aggregate.vulkan_submits = 4;
aggregate.vulkan_completions = 4;
Lardon3DResourceSequenceDiagnostic last{};
last.backend = LARDON3D_RESOURCE_BACKEND_ORB_VULKAN;
last.actual_backend = LARDON3D_RESOURCE_BACKEND_ORB_VULKAN;
last.cpu_threads = 1;
last.gpu_slots = 1;
last.batch_size = 2;
last.inflight_limit = 1;
last.io_slots = 1;
last.memory_bytes = 2 * 10 * 1024 * 1024;
last.gpu_memory_bytes = 640 * 1024;
MatcherExperimentValidation validated = validate_forced_matcher_experiment(
runtime, true, aggregate, true, last, true, 0, 0, false);
if (!validated.valid || validated.local_ineligible_fallback_items != 0)
return false;
/* The second controlled cohort differs only in admitted inflight/payload;
* selected backend, CPU/GPU/batch/helpers, and scientific output stay fixed. */
runtime.matcher_inflight_override = 2;
last.inflight_limit = 2;
last.gpu_memory_bytes = 2 * 640 * 1024;
validated = validate_forced_matcher_experiment(
runtime, true, aggregate, true, last, true, 0, 0, false);
if (!validated.valid) return false;
runtime.matcher_batch_override = 4;
last.batch_size = 4;
last.memory_bytes = 4 * 10 * 1024 * 1024;
validated = validate_forced_matcher_experiment(
runtime, true, aggregate, true, last, true, 0, 0, false);
if (!validated.valid) return false;
last.batch_size = 2;
validated = validate_forced_matcher_experiment(
runtime, true, aggregate, true, last, true, 0, 0, false);
if (validated.valid ||
std::string(validated.reason) != "forced-contract-mismatch")
return false;
runtime.matcher_batch_override = 0;
last.batch_size = 2;
last.memory_bytes = 2 * 10 * 1024 * 1024;
aggregate.actual_backend_sequences[
LARDON3D_RESOURCE_BACKEND_ORB_VULKAN] = 1;
aggregate.actual_backend_sequences[LARDON3D_RESOURCE_BACKEND_MIXED] = 1;
aggregate.backend_fallback_sequences = 1;
aggregate.backend_ineligible_fallback_sequences = 1;
aggregate.local_ineligible_fallback_items = 3;
aggregate.durable_items = 4;
validated = validate_forced_matcher_experiment(
runtime, true, aggregate, true, last, true, 0, 0, false);
if (!validated.valid || validated.local_ineligible_fallback_items != 3)
return false;
aggregate.local_ineligible_fallback_items = 0;
validated = validate_forced_matcher_experiment(
runtime, true, aggregate, true, last, true, 0, 0, false);
if (validated.valid ||
std::string(validated.reason) !=
"fallback-item-classification-mismatch")
return false;
aggregate.backend_ineligible_fallback_sequences = 0;
aggregate.backend_failure_fallback_sequences = 1;
aggregate.local_ineligible_fallback_items = 0;
aggregate.backend_failure_fallback_items = 1;
validated = validate_forced_matcher_experiment(
runtime, true, aggregate, true, last, true, 0, 0, false);
if (validated.valid || std::string(validated.reason) != "backend-failure")
return false;
aggregate.backend_failure_fallback_sequences = 0;
aggregate.backend_failure_fallback_items = 0;
aggregate.backend_other_fallback_items = 1;
validated = validate_forced_matcher_experiment(
runtime, true, aggregate, true, last, true, 0, 0, false);
if (validated.valid ||
std::string(validated.reason) != "unclassified-backend-fallback")
return false;
aggregate = {};
aggregate.admission_count = 1;
aggregate.sequence_count = 1;
aggregate.selected_backend_admissions[LARDON3D_RESOURCE_BACKEND_CPU] = 1;
aggregate.actual_backend_sequences[LARDON3D_RESOURCE_BACKEND_CPU] = 1;
validated = validate_forced_matcher_experiment(
runtime, true, aggregate, true, last, true, 0, 0, false);
return !validated.valid &&
std::string(validated.reason) ==
"selected-contract-not-exclusively-vulkan";
}
int main() {
CHECK(unsetenv("MESA_SHADER_CACHE_DISABLE") == 0);
CHECK(lardon3d_resource_governor_internal_configure_driver_policy() ==
LARDON3D_RESOURCE_DRIVER_POLICY_DEFAULTED);
CHECK(std::string(std::getenv("MESA_SHADER_CACHE_DISABLE")) == "true");
CHECK(lardon3d_resource_governor_internal_configure_driver_policy() ==
LARDON3D_RESOURCE_DRIVER_POLICY_INHERITED_SAFE);
CHECK(setenv("MESA_SHADER_CACHE_DISABLE", "false", 1) == 0);
CHECK(lardon3d_resource_governor_internal_configure_driver_policy() ==
LARDON3D_RESOURCE_DRIVER_POLICY_REJECTED_UNSAFE);
CHECK(std::string(std::getenv("MESA_SHADER_CACHE_DISABLE")) == "false");
CHECK(setenv("MESA_SHADER_CACHE_DISABLE", "true", 1) == 0);
Options options;
CHECK(parse_case({"runner", "--resume-pre-gv-existing", "--project-dir",
"/tmp/not-opened"}, options));
CHECK(options.matcher_mode == MatcherMode::kAuto &&
options.matcher_pipeline == MatcherPipeline::kRolling &&
!options.has_matcher_mode && !options.has_matcher_pipeline &&
!options.has_matcher_inflight_override &&
options.matcher_inflight_override == 0 &&
!options.has_matcher_batch_override &&
options.matcher_batch_override == 0);
options = {};
CHECK(parse_case({"runner", "--resume-pre-gv-existing", "--project-dir",
"/tmp/not-opened", "--matcher-mode", "auto",
"--matcher-pipeline", "synchronous"}, options));
CHECK(options.matcher_mode == MatcherMode::kAuto &&
options.matcher_pipeline == MatcherPipeline::kSynchronous &&
options.has_matcher_mode && options.has_matcher_pipeline);
options = {};
CHECK(parse_case({"runner", "--resume-pre-gv-existing", "--project-dir",
"/tmp/not-opened", "--matcher-mode", "vulkan",
"--matcher-pipeline", "rolling", "--gpu-budget", "1"},
options));
CHECK(options.matcher_mode == MatcherMode::kVulkan &&
options.matcher_pipeline == MatcherPipeline::kRolling);
options = {};
CHECK(parse_case({"runner", "--resume-pre-gv-existing", "--project-dir",
"/tmp/not-opened", "--matcher-mode", "auto",
"--matcher-pipeline", "rolling", "--matcher-inflight",
"1"}, options));
CHECK(options.matcher_inflight_override == 1 &&
options.has_matcher_inflight_override);
options = {};
CHECK(parse_case({"runner", "--resume-pre-gv-existing", "--project-dir",
"/tmp/not-opened", "--matcher-inflight", "2"},
options));
CHECK(options.matcher_inflight_override == 2 &&
options.has_matcher_inflight_override);
for (const unsigned batch : {2U, 4U, 8U, 12U}) {
options = {};
const std::string batch_text = std::to_string(batch);
CHECK(parse_case({"runner", "--resume-pre-gv-existing", "--project-dir",
"/tmp/not-opened", "--matcher-inflight", "1",
"--matcher-batch", batch_text.c_str()}, options));
CHECK(options.matcher_batch_override == batch &&
options.has_matcher_batch_override);
}
options = {};
CHECK(!parse_case({"runner", "--resume-pre-gv-existing", "--project-dir",
"/tmp/not-opened", "--matcher-mode", "invalid"}, options));
options = {};
CHECK(!parse_case({"runner", "--resume-pre-gv-existing", "--project-dir",
"/tmp/not-opened", "--matcher-pipeline", "invalid"},
options));
for (const char *invalid_inflight : {"0", "3", "01", "+1", "2x"}) {
options = {};
CHECK(!parse_case({"runner", "--resume-pre-gv-existing", "--project-dir",
"/tmp/not-opened", "--matcher-inflight",
invalid_inflight}, options));
}
for (const char *invalid_batch : {"1", "3", "6", "10", "16", "02",
"+2", "4x"}) {
options = {};
CHECK(!parse_case({"runner", "--resume-pre-gv-existing", "--project-dir",
"/tmp/not-opened", "--matcher-inflight", "1",
"--matcher-batch", invalid_batch}, options));
}
options = {};
CHECK(!parse_case({"runner", "--resume-candidate-existing", "--project-dir",
"/tmp/not-opened", "--matcher-mode", "auto"}, options));
options = {};
CHECK(!parse_case({"runner", "--resume-candidate-existing", "--project-dir",
"/tmp/not-opened", "--matcher-inflight", "1"},
options));
options = {};
CHECK(!parse_case({"runner", "--resume-candidate-existing", "--project-dir",
"/tmp/not-opened", "--matcher-inflight", "1",
"--matcher-batch", "2"}, options));
options = {};
CHECK(!parse_case({"runner", "--resume-geometry-existing", "--project-dir",
"/tmp/not-opened", "--cpu-budget", "2"}, options));
options = {};
CHECK(!parse_case({"runner", "--mode", "s21", "--project-dir",
"/tmp/not-opened", "--root", "/tmp", "--matcher-mode",
"auto"}, options));
/* These contradictions are rejected by main before project inspection or a
* durable Task allocation, which is the CLI contract the harness relies on. */
CHECK(invoke_case({"runner", "--resume-pre-gv-existing", "--project-dir",
"/tmp/not-opened", "--matcher-mode", "cpu",
"--matcher-pipeline", "synchronous"}) == 2);
CHECK(invoke_case({"runner", "--resume-pre-gv-existing", "--project-dir",
"/tmp/not-opened", "--matcher-mode", "vulkan",
"--gpu-budget", "0"}) == 2);
CHECK(invoke_case({"runner", "--resume-pre-gv-existing", "--project-dir",
"/tmp/not-opened", "--matcher-inflight", "2",
"--gpu-budget", "0"}) == 2);
CHECK(invoke_case({"runner", "--resume-pre-gv-existing", "--project-dir",
"/tmp/not-opened", "--matcher-mode", "cpu",
"--matcher-inflight", "1"}) == 2);
CHECK(invoke_case({"runner", "--resume-pre-gv-existing", "--project-dir",
"/tmp/not-opened", "--matcher-mode", "vulkan",
"--matcher-inflight", "1"}) == 2);
CHECK(invoke_case({"runner", "--resume-pre-gv-existing", "--project-dir",
"/tmp/not-opened", "--matcher-pipeline", "synchronous",
"--matcher-inflight", "2"}) == 2);
CHECK(invoke_case({"runner", "--resume-pre-gv-existing", "--project-dir",
"/tmp/not-opened", "--matcher-batch", "2"}) == 2);
CHECK(invoke_case({"runner", "--resume-pre-gv-existing", "--project-dir",
"/tmp/not-opened", "--matcher-pipeline", "synchronous",
"--matcher-inflight", "1", "--matcher-batch", "2"}) ==
2);
CHECK(invoke_case({"runner", "--resume-pre-gv-existing", "--project-dir",
"/tmp/not-opened", "--matcher-inflight", "1",
"--matcher-batch", "4", "--gpu-budget", "0"}) == 2);
/* The runner owns all three process controls only for one invocation. Even a
* project-validation exit must restore exact inherited values, while absent
* controls remain absent after the same path. */
CHECK(setenv(LARDON3D_MATCHER_TASK_BENCHMARK_SYNCHRONOUS_ENV,
"inherited-pipeline", 1) == 0 &&
setenv(LARDON3D_MATCHER_TASK_BENCHMARK_INFLIGHT_ENV,
"inherited-inflight", 1) == 0 &&
setenv(LARDON3D_MATCHER_TASK_BENCHMARK_BATCH_ENV,
"inherited-batch", 1) == 0 &&
invoke_case({"runner", "--resume-pre-gv-existing", "--project-dir",
"/tmp/not-opened", "--matcher-inflight", "2",
"--matcher-batch", "8"}) == 2 &&
std::string(std::getenv(
LARDON3D_MATCHER_TASK_BENCHMARK_SYNCHRONOUS_ENV)) ==
"inherited-pipeline" &&
std::string(std::getenv(
LARDON3D_MATCHER_TASK_BENCHMARK_INFLIGHT_ENV)) ==
"inherited-inflight" &&
std::string(std::getenv(
LARDON3D_MATCHER_TASK_BENCHMARK_BATCH_ENV)) ==
"inherited-batch" &&
unsetenv(LARDON3D_MATCHER_TASK_BENCHMARK_SYNCHRONOUS_ENV) == 0 &&
unsetenv(LARDON3D_MATCHER_TASK_BENCHMARK_INFLIGHT_ENV) == 0 &&
unsetenv(LARDON3D_MATCHER_TASK_BENCHMARK_BATCH_ENV) == 0 &&
invoke_case({"runner", "--resume-pre-gv-existing", "--project-dir",
"/tmp/not-opened", "--matcher-inflight", "1",
"--matcher-batch", "4"}) == 2 &&
std::getenv(LARDON3D_MATCHER_TASK_BENCHMARK_SYNCHRONOUS_ENV) ==
nullptr &&
std::getenv(LARDON3D_MATCHER_TASK_BENCHMARK_INFLIGHT_ENV) == nullptr &&
std::getenv(LARDON3D_MATCHER_TASK_BENCHMARK_BATCH_ENV) == nullptr);
CHECK(matcher_evidence_aggregate_case());
CHECK(forced_matcher_experiment_validation_case());
return EXIT_SUCCESS;
}

File diff suppressed because it is too large Load diff

View file

@ -1,5 +1,6 @@
#include <stdbool.h>
#include <fcntl.h>
#include <limits.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
@ -37,33 +38,53 @@ write_text(const char *path, const char *text)
}
static bool
create_card(const char *root, unsigned int index, const char *total, const char *used)
card_path(char path[PATH_MAX], const char *root, unsigned int index,
const char *name)
{
char card[256];
char device[256];
char path[256];
if (snprintf(card, sizeof(card), "%s/card%u", root, index) < 0
|| snprintf(device, sizeof(device), "%s/device", card) < 0
int written = snprintf(path, PATH_MAX, "%s/card%u/device/%s", root,
index, name);
return written > 0 && written < PATH_MAX;
}
static bool
create_card(const char *root, unsigned int index, const char *total,
const char *used, const char *gtt)
{
char card[PATH_MAX];
char device[PATH_MAX];
char path[PATH_MAX];
int card_written = snprintf(card, sizeof(card), "%s/card%u", root, index);
int device_written = snprintf(device, sizeof(device), "%s/device", card);
if (card_written <= 0 || (size_t)card_written >= sizeof(card)
|| device_written <= 0 || (size_t)device_written >= sizeof(device)
|| mkdir(card, 0700) != 0 || mkdir(device, 0700) != 0) {
return false;
}
(void)snprintf(path, sizeof(path), "%s/vendor", device);
if (!write_text(path, "0x1002\n")) {
if (!card_path(path, root, index, "vendor")
|| !write_text(path, "0x1002\n")) {
return false;
}
(void)snprintf(path, sizeof(path), "%s/mem_info_vram_total", device);
if (!write_text(path, total)) {
if (!card_path(path, root, index, "mem_info_vram_total")
|| !write_text(path, total)) {
return false;
}
(void)snprintf(path, sizeof(path), "%s/mem_info_vram_used", device);
return write_text(path, used);
if (!card_path(path, root, index, "mem_info_vram_used")
|| !write_text(path, used)) {
return false;
}
if (!gtt) return true;
return card_path(path, root, index, "mem_info_gtt_total")
&& write_text(path, gtt);
}
static void
remove_card(const char *root, unsigned int index)
{
char path[256];
const char *files[] = {"vendor", "mem_info_vram_total", "mem_info_vram_used"};
const char *files[] = {
"vendor", "mem_info_vram_total", "mem_info_vram_used",
"mem_info_gtt_total",
};
for (size_t i = 0; i < sizeof(files) / sizeof(files[0]); ++i) {
(void)snprintf(path, sizeof(path), "%s/card%u/device/%s", root, index, files[i]);
(void)unlink(path);
@ -79,13 +100,16 @@ test_selected_gpu_pairing(void)
{
char root[] = "/tmp/lardon3d-drm-XXXXXX";
CHECK(mkdtemp(root));
CHECK(create_card(root, 0, "1000\n", "100\n"));
CHECK(create_card(root, 1, "4000\n", "3000\n"));
Lardon3DHardwareProfile profile = {0};
CHECK(create_card(root, 0, "1000\n", "100\n", "8000\n"));
CHECK(create_card(root, 1, "4000\n", "3000\n", NULL));
Lardon3DHardwareProfile profile = {
.memory_total_bytes = 16ULL * 1024 * 1024 * 1024,
};
lardon3d_hardware_profile_detect_gpu_at_root(&profile, root);
CHECK(profile.gpu_available);
CHECK(profile.gpu_drm_card_index == 0);
CHECK(profile.gpu_memory_total_bytes == 1000);
CHECK(profile.gpu_uses_shared_memory);
Lardon3DResourceSnapshot snapshot = {.memory_available_bytes = 800};
lardon3d_resource_snapshot_capture_gpu_at_root(&profile, &snapshot, root);
CHECK(snapshot.gpu_memory_available_known);
@ -98,7 +122,7 @@ test_selected_gpu_pairing(void)
"%s/card0/device/mem_info_vram_used",
root
);
CHECK(unlink(selected_usage) == 0);
CHECK(write_text(selected_usage, "100junk\n"));
snapshot = (Lardon3DResourceSnapshot) {0};
lardon3d_resource_snapshot_capture_gpu_at_root(&profile, &snapshot, root);
CHECK(!snapshot.gpu_memory_available_known);

View file

@ -20,6 +20,8 @@ extern "C" {
#include <opencv2/features2d.hpp>
#include <opencv2/imgproc.hpp>
#include "vulkan_process_startup.h"
struct Comparison {
uint64_t queries = 0;
uint64_t index_divergences = 0;
@ -41,6 +43,25 @@ static bool fp64_requested() {
return value && std::strcmp(value, "1") == 0;
}
static bool driver_policy_gate_rejects_unsafe() {
if (setenv("MESA_SHADER_CACHE_DISABLE", "false", 1) != 0) return false;
Lardon3DOrbVulkanBackend *backend = lardon3d_orb_vulkan_backend_create();
if (!backend) return false;
float descriptor[128]{};
Lardon3DSiftTop2 output{7, 11, 13.0F, 17, 19.0F};
const Lardon3DSiftTop2 unchanged = output;
Lardon3DOrbVulkanResult result = lardon3d_sift_vulkan_top2(
backend, descriptor, 1, descriptor, 1, &output, 1);
Lardon3DOrbVulkanInfo info{};
bool ok = result == LARDON3D_ORB_VULKAN_UNAVAILABLE &&
std::memcmp(&output, &unchanged, sizeof(output)) == 0 &&
lardon3d_orb_vulkan_backend_info(backend, &info) &&
info.initialized && !info.available &&
std::strcmp(std::getenv("MESA_SHADER_CACHE_DISABLE"), "false") == 0;
lardon3d_orb_vulkan_backend_destroy(backend);
return setenv("MESA_SHADER_CACHE_DISABLE", "true", 1) == 0 && ok;
}
static std::vector<float> make_descriptors(uint32_t count, bool rootsift,
uint32_t seed) {
std::vector<float> descriptors(static_cast<size_t>(count) * 128);
@ -339,6 +360,15 @@ static bool run_distribution(bool rootsift) {
}
int main() {
if (!lardon3d_vulkan_evidence_process_startup()) {
std::fprintf(stderr,
"MESA_SHADER_CACHE_DISABLE must be true for safe CPU affinity\n");
return EXIT_FAILURE;
}
if (!driver_policy_gate_rejects_unsafe()) {
std::fprintf(stderr, "SIFT Vulkan driver process-policy gate failed\n");
return EXIT_FAILURE;
}
cv::setNumThreads(12);
return run_distribution(false) && run_distribution(true)
? EXIT_SUCCESS

View file

@ -8,6 +8,7 @@
#include <lardon3d/task.h>
#include "resource_snapshot_test_utils.h"
#include "../src/task_internal.h"
#define CHECK(condition) \
do { \
@ -79,6 +80,49 @@ failure_callback(Lardon3DTask *task, void *userdata)
return lardon3d_task_fail(task, "Erreur contrôlée.") && false;
}
typedef struct {
Lardon3DResourceGovernor *governor;
bool sequence_rejected;
bool active_reservation_released;
} AssociationMismatchProbe;
typedef struct {
size_t inflight_limit;
uint64_t gpu_memory_bytes;
} DirectAdaptiveProbe;
static bool
direct_adaptive_callback(Lardon3DTask *task, void *userdata)
{
DirectAdaptiveProbe *probe = userdata;
Lardon3DResourceCapabilitySelection selection;
Lardon3DTaskExecutionContract contract;
if (!probe
|| !lardon3d_task_internal_execution_selection(task, &selection)
|| !lardon3d_task_execution_contract(task, &contract)) {
return false;
}
probe->inflight_limit = selection.inflight_limit;
probe->gpu_memory_bytes = contract.gpu_memory_bytes;
return lardon3d_task_set_progress(task, 100, "Contrat direct observé.");
}
static bool
association_mismatch_callback(Lardon3DTask *task, void *userdata)
{
AssociationMismatchProbe *probe = userdata;
Lardon3DResourceReservation *reservation = NULL;
Lardon3DTaskExecutionContract contract;
if (!lardon3d_task_internal_test_force_sequence_association_mismatch(task)) {
return false;
}
probe->sequence_rejected = !lardon3d_task_sequence_break(
task, probe->governor, &reservation, &contract);
probe->active_reservation_released =
lardon3d_resource_governor_reservation_count(probe->governor) == 0;
return false;
}
static void *
start_task(void *context)
{
@ -118,12 +162,15 @@ run_test(void)
.logical_cpu_count = 4,
.page_size_bytes = 4096,
.memory_total_bytes = UINT64_MAX,
.gpu_available = true,
.gpu_uses_shared_memory = true,
.cpu_architecture = "test",
};
Lardon3DResourcePolicy policy = {
.maximum_cpu_load_ratio = 1.0,
.maximum_io_pressure_avg10 = 100.0,
.io_slot_capacity = 1,
.gpu_slot_capacity = 1,
};
Lardon3DResourceGovernor *governor = lardon3d_resource_governor_create(
&profile,
@ -240,6 +287,63 @@ run_test(void)
&& failed_probe.reservation_released);
lardon3d_task_destroy(task);
AssociationMismatchProbe mismatch = {.governor = governor};
task = lardon3d_task_create(
"Association invalide", &estimate, association_mismatch_callback,
&mismatch);
CHECK(task);
CHECK(lardon3d_test_resource_snapshot_make_fresh(&resource_snapshot));
CHECK(lardon3d_resource_governor_reserve(
governor, &resource_snapshot, &estimate, &decision, &reservation));
CHECK(lardon3d_task_start(task, governor, reservation));
CHECK(mismatch.sequence_rejected && mismatch.active_reservation_released);
CHECK(!lardon3d_task_internal_test_has_reservation_ownership(task));
CHECK(lardon3d_task_internal_test_association_failure_releases(task) == 1);
CHECK(lardon3d_resource_governor_reservation_count(governor) == 0);
CHECK(lardon3d_task_snapshot(task, &snapshot));
CHECK(snapshot.state == TASK_FAILED);
lardon3d_task_destroy(task);
/* A direct public reserve/start call has no private capability decision.
* It must therefore install the durable depth-one minimum, never the
* adaptive maximum that only Queue-owned admission may reserve. */
const Lardon3DResourceEstimate adaptive_estimate = {
.gpu_memory_fixed_bytes = 640 * 1024,
.minimum_batch_size = 1,
.maximum_batch_size = 1,
.desired_cpu_threads = 1,
.desired_gpu_slots = 1,
.task_class = LARDON3D_RESOURCE_TASK_MIXED,
};
Lardon3DTaskCapabilityEnvelope adaptive_envelope = {
.count = 1,
.capabilities = {{
.estimate = adaptive_estimate,
.backend = LARDON3D_RESOURCE_BACKEND_ORB_VULKAN,
.inflight_limit = 2,
.minimum_inflight_limit = 1,
.gpu_memory_bytes_per_inflight = 640 * 1024,
.inflight_adaptive = true,
}},
};
adaptive_envelope.capabilities[0].estimate.gpu_memory_fixed_bytes = 0;
DirectAdaptiveProbe direct_probe = {0};
task = lardon3d_task_create_typed(
"Contrat adaptatif direct", &adaptive_estimate,
"test.direct.adaptive", 1, direct_adaptive_callback, &direct_probe,
NULL);
CHECK(task
&& lardon3d_task_internal_set_capability_envelope(
task, &adaptive_envelope));
CHECK(lardon3d_test_resource_snapshot_make_fresh(&resource_snapshot));
CHECK(lardon3d_resource_governor_reserve(
governor, &resource_snapshot, &adaptive_estimate, &decision,
&reservation));
CHECK(reservation && lardon3d_task_start(task, governor, reservation));
CHECK(direct_probe.inflight_limit == 1
&& direct_probe.gpu_memory_bytes == 640 * 1024);
lardon3d_task_destroy(task);
task = lardon3d_task_create(
"Pause avant départ",
&estimate,

View file

@ -115,6 +115,88 @@ snapshot(void)
return result;
}
static bool
run_legacy_estimate_validation_test(void)
{
static const Lardon3DTaskKindDescriptor descriptors[] = {
{.kind = "candidate_pair.generate", .kind_version = 1,
.reconstruct = reconstruct},
{.kind = "features.extract.sift", .kind_version = 1,
.reconstruct = reconstruct},
{.kind = "features.extract.rootsift", .kind_version = 1,
.reconstruct = reconstruct},
};
Lardon3DTaskKindRegistry registry;
CHECK(lardon3d_task_kind_registry_init(&registry, descriptors, 3));
int destroyed = 0;
int finished = 0;
ReconstructContext context = {
.destroyed = &destroyed,
.finished = &finished,
};
const char *kinds[] = {
"candidate_pair.generate",
"features.extract.sift",
"features.extract.rootsift",
};
for (size_t index = 0; index < 3; ++index) {
Lardon3DTaskDurableSnapshot corrupt = snapshot();
corrupt.estimate = (Lardon3DResourceEstimate) {0};
Lardon3DTask *task = NULL;
/* An absent legacy form must never make the all-zero estimate look
* like an exact historical signature. Reconstruction owns and frees
* the binding userdata on this corruption rejection. */
CHECK(lardon3d_task_kind_registry_restore(
&registry, kinds[index], 1, &corrupt, &context, &task
) == LARDON3D_TASK_KIND_RECONSTRUCTION_FAILED);
CHECK(!task && destroyed == (int)index + 1);
}
const Lardon3DResourceEstimate historical[] = {
{
.memory_fixed_bytes = 128 * 1024,
.memory_bytes_per_item = 64 * 1024,
.minimum_batch_size = 1,
.maximum_batch_size = 64,
.desired_cpu_threads = 1,
.desired_io_slots = 1,
.task_class = LARDON3D_RESOURCE_TASK_CPU,
},
{
.memory_fixed_bytes = 64ULL * 1024 * 1024,
.memory_bytes_per_item = 1024ULL * 1024 * 1024,
.minimum_batch_size = 1,
.maximum_batch_size = 1,
.desired_cpu_threads = 1,
.desired_io_slots = 1,
.task_class = LARDON3D_RESOURCE_TASK_CPU,
},
{
.memory_fixed_bytes = 64ULL * 1024 * 1024,
.memory_bytes_per_item = 1024ULL * 1024 * 1024,
.minimum_batch_size = 1,
.maximum_batch_size = 1,
.desired_cpu_threads = 1,
.desired_io_slots = 1,
.task_class = LARDON3D_RESOURCE_TASK_CPU,
},
};
for (size_t index = 0; index < 3; ++index) {
Lardon3DTaskDurableSnapshot durable = snapshot();
durable.estimate = historical[index];
Lardon3DTask *task = NULL;
CHECK(lardon3d_task_kind_registry_restore(
&registry, kinds[index], 1, &durable, &context, &task
) == LARDON3D_TASK_KIND_OK);
Lardon3DResourceEstimate effective;
CHECK(task && lardon3d_task_resource_estimate(task, &effective));
CHECK(effective.desired_cpu_threads == 12);
lardon3d_task_destroy(task);
}
CHECK(destroyed == 6);
return true;
}
static bool
run_test(void)
{
@ -209,5 +291,6 @@ run_test(void)
int
main(void)
{
return run_test() ? EXIT_SUCCESS : EXIT_FAILURE;
return (run_test() && run_legacy_estimate_validation_test())
? EXIT_SUCCESS : EXIT_FAILURE;
}

View file

@ -1,12 +1,20 @@
#ifndef _GNU_SOURCE
#define _GNU_SOURCE
#endif
#include <limits.h>
#include <pthread.h>
#include <sched.h>
#include <stdbool.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>
#include <lardon3d/task_queue.h>
#include "../src/resource_governor_internal.h"
#include "../src/task_internal.h"
#include "resource_snapshot_test_utils.h"
#define CHECK(condition) \
@ -484,11 +492,26 @@ run_test(void)
&reduced
);
uint64_t reduced_id;
CHECK(reduced_task);
Lardon3DTaskCapabilityEnvelope reduced_envelope = {
.count = 1,
.capabilities = {{
.estimate = reduced_estimate,
.backend = LARDON3D_RESOURCE_BACKEND_CPU,
.inflight_limit = 1,
.cpu_reducible = true,
}},
};
/* This synthetic callback explicitly consumes the contract; unlike a
* production kind, it has no registry entry from which to reconstruct the
* otherwise private adaptive envelope. */
CHECK(reduced_task && lardon3d_task_internal_set_capability_envelope(
reduced_task, &reduced_envelope));
CHECK(lardon3d_task_queue_add(queue, reduced_task, &reduced_id));
CHECK(wait_terminal(queue, reduced_id, &snapshot));
CHECK(snapshot.state == TASK_COMPLETED);
CHECK(reduced.contract.cpu_threads == 1024);
/* CPU-reducible capabilities slow-start at one participant. A later
* sequence may grow only after two baseline and two improving samples. */
CHECK(reduced.contract.cpu_threads == 1);
Lardon3DResourceEstimate rejected_estimate = {
.memory_fixed_bytes = UINT64_MAX,
.memory_bytes_per_item = 1,
@ -926,10 +949,363 @@ test_stress_concurrent(Lardon3DResourceGovernor *governor)
return true;
}
typedef struct {
Lardon3DResourceGovernor *governor;
Lardon3DTaskExecutionContract first;
Lardon3DTaskExecutionContract unchanged;
Lardon3DTaskExecutionContract second;
Lardon3DResourceCapabilitySelection first_selection;
Lardon3DResourceCapabilitySelection unchanged_selection;
} ImmutableSequenceWork;
#ifdef __linux__
typedef struct {
cpu_set_t worker_mask;
cpu_set_t child_mask;
bool child_started;
} AffinityWork;
static void *
capture_child_affinity(void *userdata)
{
AffinityWork *work = userdata;
work->child_started = sched_getaffinity(
0, sizeof(work->child_mask), &work->child_mask) == 0;
return NULL;
}
static bool
affinity_callback(Lardon3DTask *task, void *userdata)
{
AffinityWork *work = userdata;
pthread_t child;
if (sched_getaffinity(0, sizeof(work->worker_mask), &work->worker_mask) != 0
|| pthread_create(&child, NULL, capture_child_affinity, work) != 0
|| pthread_join(child, NULL) != 0 || !work->child_started) {
return false;
}
return lardon3d_task_set_progress(task, 100, "Affinité observée.");
}
static bool
test_worker_only_affinity(void)
{
cpu_set_t main_before;
CPU_ZERO(&main_before);
if (sched_getaffinity(0, sizeof(main_before), &main_before) != 0) {
return true;
}
Lardon3DResourceCpuTopologyInput topology = {
.affinity_available = true,
.topology_available = true,
};
for (unsigned int cpu = 0; cpu < CPU_SETSIZE
&& cpu < LARDON3D_RESOURCE_CPU_MAX; ++cpu) {
if (!CPU_ISSET((size_t)cpu, &main_before)) {
continue;
}
size_t index = topology.allowed_cpu_count++;
topology.allowed_cpu_ids[index] = cpu;
topology.topology_entries[index] =
(Lardon3DResourceCpuTopologyEntry) {
.cpu_id = cpu,
.package_id = 0,
.core_id = (unsigned int)index,
};
}
topology.topology_entry_count = topology.allowed_cpu_count;
if (topology.allowed_cpu_count < 2
|| topology.allowed_cpu_count > UINT_MAX) {
return true;
}
Lardon3DHardwareProfile profile = {
.logical_cpu_count = (unsigned int)topology.allowed_cpu_count,
.page_size_bytes = 4096,
.memory_total_bytes = 16ULL * 1024 * 1024 * 1024,
.cpu_architecture = "test",
};
Lardon3DResourcePolicy policy = {
.system_cpu_reserve = 1,
.maximum_cpu_load_ratio = 1.0,
.maximum_io_pressure_avg10 = 100.0,
.io_slot_capacity = 1,
};
Lardon3DResourceEstimate estimate = {
.minimum_batch_size = 1,
.maximum_batch_size = 1,
.desired_cpu_threads = 1,
.task_class = LARDON3D_RESOURCE_TASK_CPU,
};
Lardon3DResourceGovernor *governor =
lardon3d_resource_governor_create(&profile, &policy);
CHECK(governor
&& lardon3d_resource_governor_internal_configure_cpu_topology(
governor, &topology));
Lardon3DTaskQueue *queue = lardon3d_task_queue_create(governor, 1);
CHECK(queue);
Lardon3DResourceCpuPolicyDiagnostic diagnostic;
CHECK(lardon3d_resource_governor_internal_cpu_policy(
governor, &diagnostic));
CHECK(diagnostic.affinity_attempted && diagnostic.affinity_active
&& diagnostic.compute_cpu_count == topology.allowed_cpu_count - 1);
cpu_set_t expected;
CPU_ZERO(&expected);
for (unsigned int cpu = 0; cpu < CPU_SETSIZE
&& cpu < LARDON3D_RESOURCE_CPU_MAX; ++cpu) {
if ((diagnostic.compute_mask[cpu / 64]
& (UINT64_C(1) << (cpu % 64))) != 0) {
CPU_SET((size_t)cpu, &expected);
}
}
AffinityWork work = {0};
Lardon3DTask *task = lardon3d_task_create_typed(
"Affinité worker", &estimate, "test.affinity", 1,
affinity_callback, &work, NULL);
uint64_t id = 0;
CHECK(task && lardon3d_task_queue_add(queue, task, &id));
Lardon3DTaskSnapshot snapshot;
CHECK(wait_terminal(queue, id, &snapshot)
&& snapshot.state == TASK_COMPLETED
&& CPU_EQUAL(&work.worker_mask, &expected)
&& CPU_EQUAL(&work.child_mask, &expected));
cpu_set_t main_after;
CPU_ZERO(&main_after);
CHECK(sched_getaffinity(0, sizeof(main_after), &main_after) == 0
&& CPU_EQUAL(&main_before, &main_after));
lardon3d_task_queue_destroy(queue);
lardon3d_resource_governor_destroy(governor);
/* An injected application failure must restore/retain the inherited mask,
* remain observable, and still permit Queue-owned Task cleanup. */
governor = lardon3d_resource_governor_create(&profile, &policy);
CHECK(governor
&& lardon3d_resource_governor_internal_configure_cpu_topology(
governor, &topology));
lardon3d_resource_governor_internal_force_worker_affinity_failure(
governor, true);
queue = lardon3d_task_queue_create(governor, 1);
CHECK(queue && lardon3d_resource_governor_internal_cpu_policy(
governor, &diagnostic));
CHECK(diagnostic.affinity_attempted && !diagnostic.affinity_active
&& strcmp(diagnostic.reason, "worker-affinity-apply-failed") == 0);
work = (AffinityWork) {0};
task = lardon3d_task_create_typed(
"Échec affinité worker", &estimate, "test.affinity.failure", 1,
affinity_callback, &work, NULL);
id = 0;
CHECK(task && lardon3d_task_queue_add(queue, task, &id)
&& wait_terminal(queue, id, &snapshot)
&& snapshot.state == TASK_COMPLETED
&& CPU_EQUAL(&work.worker_mask, &main_before)
&& CPU_EQUAL(&work.child_mask, &main_before));
lardon3d_task_queue_destroy(queue);
lardon3d_resource_governor_destroy(governor);
return true;
}
#else
static bool test_worker_only_affinity(void) { return true; }
#endif
static bool
immutable_sequence_callback(Lardon3DTask *task, void *userdata)
{
ImmutableSequenceWork *work = userdata;
if (!lardon3d_task_execution_contract(task, &work->first)
|| !lardon3d_task_internal_execution_selection(
task, &work->first_selection)
|| work->first.gpu_slots != 1
|| work->first_selection.inflight_limit != 1
|| !lardon3d_resource_governor_internal_set_backend_available(
work->governor,
LARDON3D_RESOURCE_BACKEND_ORB_VULKAN,
false
)
|| !lardon3d_task_execution_contract(task, &work->unchanged)
|| !lardon3d_task_internal_execution_selection(
task, &work->unchanged_selection)
|| work->first.batch_size != work->unchanged.batch_size
|| work->first.memory_bytes != work->unchanged.memory_bytes
|| work->first.gpu_memory_bytes != work->unchanged.gpu_memory_bytes
|| work->first.cpu_threads != work->unchanged.cpu_threads
|| work->first.gpu_slots != work->unchanged.gpu_slots
|| work->first.io_slots != work->unchanged.io_slots
|| work->first_selection.inflight_limit
!= work->unchanged_selection.inflight_limit) {
return false;
}
Lardon3DResourceReservation *reservation = NULL;
return lardon3d_task_sequence_break(
task,
work->governor,
&reservation,
&work->second
)
&& work->second.gpu_slots == 0 && work->second.cpu_threads == 1;
}
static bool
test_sequence_contract_immutability(void)
{
Lardon3DHardwareProfile profile = {
.logical_cpu_count = 8,
.page_size_bytes = 4096,
.memory_total_bytes = 16ULL * 1024 * 1024 * 1024,
.gpu_available = true,
.gpu_uses_shared_memory = true,
.cpu_architecture = "test",
};
Lardon3DResourcePolicy policy = {
.maximum_cpu_load_ratio = 1.0,
.maximum_io_pressure_avg10 = 100.0,
.gpu_slot_capacity = 1,
.io_slot_capacity = 1,
};
Lardon3DResourceGovernor *governor =
lardon3d_resource_governor_create(&profile, &policy);
CHECK(governor);
CHECK(lardon3d_resource_governor_internal_set_backend_available(
governor, LARDON3D_RESOURCE_BACKEND_ORB_VULKAN, true));
Lardon3DTaskQueue *queue = lardon3d_task_queue_create(governor, 1);
CHECK(queue);
Lardon3DResourceEstimate cpu = {
.memory_bytes_per_item = 1024 * 1024,
.minimum_batch_size = 1,
.maximum_batch_size = 1,
.desired_cpu_threads = 4,
.task_class = LARDON3D_RESOURCE_TASK_CPU,
};
Lardon3DResourceEstimate gpu = cpu;
gpu.gpu_memory_fixed_bytes = 0;
gpu.desired_cpu_threads = 1;
gpu.desired_gpu_slots = 1;
Lardon3DTaskCapabilityEnvelope envelope = {
.count = 2,
.capabilities = {
{
.estimate = gpu,
.backend = LARDON3D_RESOURCE_BACKEND_ORB_VULKAN,
.inflight_limit = 2,
.minimum_inflight_limit = 1,
.gpu_memory_bytes_per_inflight = 640 * 1024,
.preferred = true,
.inflight_adaptive = true,
.requires_runtime_backend = true,
},
{
.estimate = cpu,
.backend = LARDON3D_RESOURCE_BACKEND_CPU,
.inflight_limit = 1,
.cpu_reducible = true,
},
},
};
ImmutableSequenceWork work = {.governor = governor};
Lardon3DTask *task = lardon3d_task_create_typed(
"Contrat immuable",
&cpu,
"test.sequence",
1,
immutable_sequence_callback,
&work,
NULL
);
CHECK(task && lardon3d_task_internal_set_capability_envelope(task, &envelope));
uint64_t id = 0;
CHECK(lardon3d_task_queue_add(queue, task, &id));
Lardon3DTaskSnapshot snapshot;
CHECK(wait_terminal(queue, id, &snapshot));
CHECK(snapshot.state == TASK_COMPLETED && work.first.gpu_slots == 1
&& work.second.gpu_slots == 0);
lardon3d_task_queue_destroy(queue);
lardon3d_resource_governor_destroy(governor);
return true;
}
typedef struct {
bool called;
unsigned int cpu_threads;
} CpuEnvelopeWork;
static bool
cpu_envelope_callback(Lardon3DTask *task, void *userdata)
{
CpuEnvelopeWork *work = userdata;
Lardon3DTaskExecutionContract contract;
if (!work || !lardon3d_task_execution_contract(task, &contract)) {
return false;
}
work->called = true;
work->cpu_threads = contract.cpu_threads;
return true;
}
static bool
test_fixed_default_and_validated_cpu_range(void)
{
/* An unknown kind must not acquire CPU adaptation merely because its
* durable estimate asks for several threads. Only registered callbacks
* whose output was validated across counts may consume a reduced count. */
Lardon3DHardwareProfile profile = {
.logical_cpu_count = 1024,
.page_size_bytes = 4096,
.memory_total_bytes = 16ULL * 1024 * 1024 * 1024,
.cpu_architecture = "test",
};
Lardon3DResourcePolicy policy = {
.system_cpu_reserve = 1022,
.maximum_cpu_load_ratio = 1.0,
.maximum_io_pressure_avg10 = 100.0,
.io_slot_capacity = 1,
};
Lardon3DResourceGovernor *governor =
lardon3d_resource_governor_create(&profile, &policy);
CHECK(governor);
Lardon3DResourceEstimate estimate = {
.memory_bytes_per_item = 1024,
.minimum_batch_size = 1,
.maximum_batch_size = 1,
.desired_cpu_threads = 4,
.desired_io_slots = 1,
.task_class = LARDON3D_RESOURCE_TASK_CPU,
};
CpuEnvelopeWork fixed_work = {0};
Lardon3DTask *fixed = lardon3d_task_create_typed(
"Enveloppe fixe", &estimate, "test.fixed", 1,
cpu_envelope_callback, &fixed_work, NULL);
CHECK(fixed);
Lardon3DResourceDecision decision;
Lardon3DResourceReservation *reservation = NULL;
CHECK(lardon3d_task_internal_reserve_available(
fixed, governor, &decision, &reservation));
CHECK(decision.kind == LARDON3D_RESOURCE_WAIT && !reservation
&& !fixed_work.called);
lardon3d_task_destroy(fixed);
CpuEnvelopeWork adaptive_work = {0};
Lardon3DTask *adaptive = lardon3d_task_create_typed(
"Enveloppe validée", &estimate, "features.extract", 1,
cpu_envelope_callback, &adaptive_work, NULL);
CHECK(adaptive);
CHECK(lardon3d_task_internal_reserve_available(
adaptive, governor, &decision, &reservation));
CHECK((decision.kind == LARDON3D_RESOURCE_START
|| decision.kind == LARDON3D_RESOURCE_REDUCE_BATCH)
&& reservation);
CHECK(lardon3d_task_start(adaptive, governor, reservation));
CHECK(adaptive_work.called && adaptive_work.cpu_threads == 1);
(void)lardon3d_resource_governor_release(governor, reservation);
lardon3d_task_destroy(adaptive);
lardon3d_resource_governor_destroy(governor);
return true;
}
int
main(void)
{
if (!run_test()) {
if (!run_test() || !test_worker_only_affinity()
|| !test_sequence_contract_immutability()
|| !test_fixed_default_and_validated_cpu_range()) {
return EXIT_FAILURE;
}
Lardon3DHardwareProfile profile = {

View file

@ -0,0 +1,22 @@
#ifndef LARDON3D_TESTS_VULKAN_PROCESS_STARTUP_H
#define LARDON3D_TESTS_VULKAN_PROCESS_STARTUP_H
#include <cstdlib>
#include <cstring>
/* Standalone Vulkan evidence tools own this process-start action. Call it as
* the first statement of main, before OpenCV or any other library may create a
* pthread. An absent value gets the safe default; an explicit value is never
* overwritten and must already be exact true/1. The production backend itself
* remains a non-mutating late boundary. */
static inline bool lardon3d_vulkan_evidence_process_startup() {
const char *value = std::getenv("MESA_SHADER_CACHE_DISABLE");
if (!value) {
if (setenv("MESA_SHADER_CACHE_DISABLE", "true", 0) != 0) return false;
value = std::getenv("MESA_SHADER_CACHE_DISABLE");
}
return value &&
(std::strcmp(value, "true") == 0 || std::strcmp(value, "1") == 0);
}
#endif