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:
parent
663176f884
commit
599be97fbe
61 changed files with 15491 additions and 714 deletions
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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 0–15 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
|
||||
|
|
|
|||
|
|
@ -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`.
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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 129–136 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
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
||||
|
|
|
|||
85
meson.build
85
meson.build
|
|
@ -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',
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
23
src/app.c
23
src/app.c
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
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) {
|
||||
try {
|
||||
int threads = cv::getNumThreads();
|
||||
return threads > 0 ? static_cast<unsigned int>(threads) : 1U;
|
||||
} catch (...) {
|
||||
return 1U;
|
||||
}
|
||||
}
|
||||
|
||||
extern "C" bool
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
*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};
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
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, text, capacity - 1);
|
||||
count = read(descriptor, &extra, 1);
|
||||
} while (count < 0 && errno == EINTR);
|
||||
bool success = count >= 0 && close(descriptor) == 0;
|
||||
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, >t_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;
|
||||
|
|
|
|||
305
src/matcher.cpp
305
src/matcher.cpp
|
|
@ -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;
|
||||
}
|
||||
try {
|
||||
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);
|
||||
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;
|
||||
/* 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;
|
||||
return LARDON3D_MATCHER_FAILED;
|
||||
}
|
||||
}
|
||||
|
||||
extern "C" Lardon3DMatcherResult lardon3d_matcher_match_and_publish(
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
1204
src/matcher_task.c
1204
src/matcher_task.c
File diff suppressed because it is too large
Load diff
16
src/matcher_task_benchmark_internal.h
Normal file
16
src/matcher_task_benchmark_internal.h
Normal 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
|
||||
46
src/opencv_task_thread_control.h
Normal file
46
src/opencv_task_thread_control.h
Normal 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
|
||||
69
src/opencv_task_thread_guard.h
Normal file
69
src/opencv_task_thread_guard.h
Normal 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
121
src/orb_vulkan_backend_internal.h
Normal file
121
src/orb_vulkan_backend_internal.h
Normal 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
|
||||
|
|
@ -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."); }
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
*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, 1, elapsed_ns(begin, end), 0);
|
||||
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};
|
||||
|
|
|
|||
464
src/task.c
464
src/task.c
|
|
@ -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
98
src/task_internal.h
Normal 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
|
||||
|
|
@ -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)) {
|
||||
|
|
|
|||
|
|
@ -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,13 +97,11 @@ 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(
|
||||
bool evaluated = lardon3d_task_internal_reserve_available(
|
||||
node->task,
|
||||
queue->governor,
|
||||
&estimate,
|
||||
&decision,
|
||||
&candidate
|
||||
);
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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) !=
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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
|
|
@ -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';
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
37
tests/test_driver_policy_startup.py
Normal file
37
tests/test_driver_policy_startup.py
Normal 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())
|
||||
|
|
@ -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, ¶meters,
|
||||
&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, ¶meters,
|
||||
&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(¶meters, 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, ¶meters, &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, ¶meters, &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);
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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, ¶meters, 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;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
|
@ -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);
|
||||
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([&] {
|
||||
result_a = lardon3d_orb_vulkan_top2(backend, a.data(), 1024, b.data(), 1024,
|
||||
actual_a.data(), actual_a.size());
|
||||
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([&] {
|
||||
result_b = lardon3d_orb_vulkan_top2(backend, b.data(), 1024, a.data(), 1024,
|
||||
actual_b.data(), actual_b.size());
|
||||
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();
|
||||
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);
|
||||
/* 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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
495
tests/test_pre_sfm_runner_options.cpp
Normal file
495
tests/test_pre_sfm_runner_options.cpp
Normal 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
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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(®istry, 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(
|
||||
®istry, 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(
|
||||
®istry, 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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 = {
|
||||
|
|
|
|||
22
tests/vulkan_process_startup.h
Normal file
22
tests/vulkan_process_startup.h
Normal 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
|
||||
Loading…
Reference in a new issue