Files
Ange-Marie MAURIN - A2M Systèmes 6cf0c54e3b feat(prototypes): étape 2 gpu_interop exécutée et mesurée
CUDA 13.5 : import 0,07 ms, mappage 0,05-0,08 ms, copie GPU->GPU 0,06 ms (100-140 Go/s) contre ~32 ms par le CPU, vérification octet à octet OK. Corrige aussi un nom de test contenant ';' que ctest découpait en deux.
2026-11-06 21:07:10 +01:00
..

PROTOTYPE JETABLE — pas du code produit. Ce dossier sert uniquement à valider une hypothèse technique. Il n'a ni les garanties, ni l'API, ni la qualité d'ingénierie attendues du reste du dépôt, et il sera supprimé ou réécrit.

gpu_interop — partage d'une texture Vulkan vers Vulkan et CUDA (étapes 1 et 2)

SEQUA Engine en bref

SEQUA signifie Simulate Environments, Quantify Uncertainty, Adapt : l'engine simule des environnements, quantifie l'incertitude de leur représentation et adapte les méthodes de calcul et de rendu au contexte. Le produit s'articule en quatre couches : SEQUA Studio (l'outil de création), SEQUA Core (format de projet, simulation déterministe, orchestration), SEQUA 3D (le contrat de données et les adaptateurs) et des moteurs 3D interchangeables (Godot, Unreal, puis un moteur maison) qui exécutent le rendu sans contenir la logique du produit. Le rendu neuronal et génératif est un mode optionnel, piloté par des budgets et des cartes de confiance.

Rôle de ce prototype dans l'engine

Le risque n°1 identifié au §5.6 de docs/SEQUA_Engine_resume_complet_v2.md est la fuite des abstractions au passage GPU rendu → inférence :

« Les données restent résidentes sur le GPU autant que possible. Le passage de textures entre l'API graphique (Vulkan, DirectX) et le moteur d'inférence (CUDA, TensorRT) sans copie CPU est le point où les abstractions fuient le plus ; il est testé en tout premier. »

Ce prototype vérifie donc la brique la plus basse et la plus décisive : une texture produite par un contexte Vulkan peut-elle être exportée puis réutilisée par un autre contexte GPU (Vulkan, puis CUDA) sans jamais transiter par la mémoire système ? Si l'étape 1 ou 2 échoue, le contrat NeuralFrameContext (§5.6) doit prévoir des copies CPU, ce qui change l'architecture et les budgets.

Étapes prévues

Étape Contenu État
1 Export d'une VkImage en HANDLE Win32 (OPAQUE_WIN32), import dans un second VkDevice, comparaison octet à octet, mesures (gpu_interop_vk_export) fait et mesuré
2 Import de la même allocation dans CUDA via cudaImportExternalMemory / cudaExternalMemoryGetMappedMipmappedArray, copie GPU→GPU mesurée et vérification (gpu_interop_vk_cuda) fait et mesuré (CUDA 13.5)
3 Inférence ONNX Runtime (ou TensorRT) directement sur le tampon importé, sans copie CPU à faire
4 Synchronisation fine par sémaphores externes (VK_KHR_external_semaphore_win32 / cudaExternalSemaphore), suppression des vkQueueWaitIdle à faire — l'extension est disponible sur le GPU de test, vérifiée et affichée par l'étape 1

Le code Vulkan commun aux deux étapes (instance, sélection du GPU, device headless, image exportable, staging, motif déterministe, export du HANDLE, RAII, aller-retour de référence) est factorisé dans vk_common.hpp / vk_common.cpp. main.cpp (étape 1) et vk_cuda.cpp (étape 2) ne gardent que leur logique propre ; l'étape 1 se comporte exactement comme avant la factorisation (même sortie, mêmes mesures).

Étape 1 — gpu_interop_vk_export

  1. Crée une instance Vulkan headless (aucune fenêtre, aucune surface) sur API 1.2.
  2. Sélectionne un GPU avec file graphique et VK_KHR_external_memory_win32, puis crée deux VkDevice distincts sur le même VkPhysicalDevice, et vérifie que leurs deviceUUID concordent.
  3. Alloue une VkImage 1920×1080 VK_FORMAT_R8G8B8A8_UNORM en mémoire DEVICE_LOCAL, avec VkExternalMemoryImageCreateInfo + VkExportMemoryAllocateInfo (VK_EXTERNAL_MEMORY_HANDLE_TYPE_OPAQUE_WIN32_BIT) et une allocation dédiée.
  4. La remplit avec un motif déterministe (R = x & 0xFF, G = y & 0xFF, B = (x ^ y) & 0xFF, A = 0xFF) par staging buffer + vkCmdCopyBufferToImage — aucun shader, donc aucune dépendance glslang.
  5. Exporte la mémoire avec vkGetMemoryWin32HandleKHR, importe le HANDLE dans le second device (VkImportMemoryWin32HandleInfoKHR), lie une VkImage identique à la mémoire importée.
  6. Relit l'image du second device via un staging buffer et compare octet à octet au motif.
  7. Mesure l'export, l'import et, à titre de référence, un aller-retour GPU → CPU → GPU complet de la même image (5 essais, minimum et médiane).
  8. Ferme le HANDLE avec CloseHandle et libère toutes les ressources (RAII).

Code de retour : 0 si les 8 294 400 octets sont identiques, 2 si le contenu diffère, 1 sur erreur Vulkan ou d'initialisation.

Étape 2 — gpu_interop_vk_cuda

C++ pur : aucun .cu, aucun noyau CUDA, uniquement l'API runtime (CUDA::cudart). Le programme réutilise vk_common.hpp / vk_common.cpp (instance, device headless, image exportable, motif, staging, export du HANDLE, RAII).

  1. Crée l'image Vulkan exportable remplie du motif et exporte son HANDLE OPAQUE_WIN32.
  2. Énumère les devices CUDA (cudaGetDeviceCount / cudaGetDeviceProperties) et choisit celui dont cudaDeviceProp::uuid est égal au deviceUUID Vulkan (VkPhysicalDeviceIDProperties). Échec explicite, avec la liste des UUID vus, si aucun ne correspond.
  3. Importe la mémoire avec cudaImportExternalMemory (cudaExternalMemoryHandleTypeOpaqueWin32) en passant size = taille de l'allocation Vulkan et cudaExternalMemoryDedicated uniquement parce que l'allocation Vulkan est dédiée (VkMemoryDedicatedAllocateInfo dans create_filled_exportable_image ; le drapeau est propagé depuis ExportableImage::dedicated).
  4. Mappe l'image (tiling OPTIMAL) avec cudaExternalMemoryGetMappedMipmappedArray : format de canal uchar4 (8 bits × 4 non signé), extent 1920×1080 (depth 0 = tableau 2D), 1 niveau, puis cudaGetMipmappedArrayLevel(…, 0).
  5. Copie le niveau 0 vers un tampon device avec cudaMemcpy2DFromArray — c'est le chemin mesuré GPU → GPU : std::chrono autour de l'appel et cudaEvent pour le temps GPU.
  6. Pour la vérification uniquement, copie le tampon device vers l'hôte et compare octet à octet au motif (cette copie est hors du chemin mesuré).
  7. Mesure séparément l'import, le mappage et la copie, puis rappelle l'aller-retour GPU → CPU → GPU de l'étape 1 comme référence.
  8. Libère tout : cudaFree, cudaFreeMipmappedArray, cudaDestroyExternalMemory, CloseHandle.

Les ressources CUDA sont libérées dans une portée dédiée avant l'aller-retour de référence Vulkan : Vulkan réécrit l'image ensuite, donc plus aucun objet CUDA ne doit la référencer. Le CloseHandle intervient en dernier, après la destruction de l'objet cudaExternalMemory (CUDA garde sa propre référence sur le handle ; il ne prend pas possession de celui de l'application).

Synchronisation : vkQueueWaitIdle avant l'export suffit pour cette étape. Les sémaphores externes (VK_KHR_external_semaphore_win32 / cudaExternalSemaphore) sont l'étape 4 ; le programme ne prétend donc pas mesurer un pipeline asynchrone de production.

Construire et lancer

Prérequis étape 1 : Windows 10/11, Visual Studio 2022 avec les outils C++ x64, Vulkan SDK (testé avec 1.4.341.1), un GPU exposant VK_KHR_external_memory_win32.

Prérequis étape 2 : CUDA Toolkit (13.5 sur la machine de référence) et un GPU dont l'UUID CUDA correspond à celui exposé par Vulkan. Sans CUDA Toolkit, la cible est simplement ignorée et le reste du dépôt continue de se configurer et de se tester (find_package(CUDAToolkit QUIET) puis message STATUS). Quand la cible est construite, ctest reçoit automatiquement le dossier bin du Toolkit dans son PATH (ENVIRONMENT_MODIFICATION), afin de trouver cudart64_*.dll même si le shell appelant n'a pas encore hérité du PATH mis à jour par l'installateur.

Depuis la racine du dépôt :

powershell.exe -NoProfile -ExecutionPolicy Bypass -File scripts\build.ps1 -Preset windows-msvc-prototypes

Le script configure, compile et lance ctest. Pour exécuter seulement les prototypes :

ctest --preset windows-msvc-prototypes -L prototype --output-on-failure

Étape 2 seulement (si elle a été construite) :

ctest --preset windows-msvc-prototypes -L cuda --output-on-failure

Ou directement :

.\build\windows-msvc-prototypes\prototypes\gpu_interop\gpu_interop_vk_export.exe
.\build\windows-msvc-prototypes\prototypes\gpu_interop\gpu_interop_vk_cuda.exe

Sous Linux/macOS, prototypes/CMakeLists.txt n'ajoute pas les cibles et affiche un message STATUS : le code de ces étapes est Windows uniquement.

Résultats mesurés

Machine de référence : Windows 11, NVIDIA GeForce RTX 4090 Laptop GPU (16 Go), pilote Vulkan 1.4.357, MSVC 14.44, build RelWithDebInfo, 6 novembre 2026.

Image : 1920×1080 R8G8B8A8_UNORM, 8 294 400 octets utiles, allocation de 8 847 360 octets.

Étape 1 (mesurée)

Mesure Valeur
Export vkGetMemoryWin32HandleKHR 0,000 – 0,001 ms
Import dans le second VkDevice (création image + vkAllocateMemory + vkBindImageMemory) 0,045 – 0,135 ms (selon l'exécution)
Aller-retour GPU → CPU → GPU de référence (médiane de 5 essais) 30,5 – 37,0 ms
Comparaison octet à octet après import identique (8 294 400 / 8 294 400 octets)

Extrait d'exécution :

[3] Image A : memoire DEVICE_LOCAL exportable (type 1, 8847360 octets)
[4] Motif deterministe uploade (staging buffer + vkCmdCopyBufferToImage)
[5] Export : HANDLE Win32 0x00000000000005E4 en 0.000 ms
[6] Import dans le device B : type 1, 0.045 ms (creation image + alloc + bind)
[7] Comparaison octet a octet apres import : IDENTIQUE
[8] CloseHandle du HANDLE exporte : OK
[9] Aller-retour GPU->CPU->GPU de reference (5 essais)
      min 30.048 ms, mediane 30.528 ms, contenu identique

=== RESULTATS ===
  Export HANDLE Win32        :    0.000 ms
  Import dans un 2e device   :    0.045 ms
  Aller-retour GPU->CPU->GPU :   30.528 ms (mediane de 5 essais)
  Verification octet a octet : OK

Lecture : l'export et l'import sont des opérations de métadonnées (aucune donnée déplacée), d'où des temps négligeables devant le round-trip CPU, qui déplace réellement 2 × 8,3 Mo sur le bus et subit deux synchronisations vkQueueWaitIdle (latence WDDM). C'est exactement l'écart que l'étape 2 doit rendre inutile.

Étape 2 (exécutée le 2026-11-06)

RTX 4090 Laptop, pilote 621.01, CUDA Toolkit 13.5.37, MSVC 14.44, preset windows-msvc-prototypes. Compilation sans avertissement (/W4 /WX), test gpu_interop_vk_cuda vert. Trois exécutions :

Mesure Exéc. 1 Exéc. 2 Exéc. 3
Import CUDA (cudaImportExternalMemory) 0,071 ms 0,074 ms 0,076 ms
Mappage mipmapped array 0,082 ms 0,053 ms 0,059 ms
Copie GPU → GPU array → tampon (cudaEvent) 0,059 ms 0,081 ms 0,063 ms
Débit de la copie 139,7 Go/s 102,5 Go/s 130,6 Go/s
Aller-retour GPU → CPU → GPU de référence (médiane) 31,98 ms 32,68 ms 32,68 ms
Vérification octet à octet OK OK OK

Lecture : une texture Vulkan 1920×1080 RGBA8 devient lisible par CUDA en ~0,15 ms (import + mappage, une seule fois par ressource), puis chaque copie GPU → GPU coûte ~0,06 ms, contre ~32 ms par le CPU. Le temps chrono de la copie (0,4–0,5 ms) inclut le lancement et la synchronisation de l'hôte ; la valeur cudaEvent mesure le travail GPU seul.

Limites et points non vérifiés

  • Import d'un handle OPAQUE_WIN32 : vkGetMemoryWin32HandlePropertiesKHR ne doit pas être appelé pour ce type de handle (VUID-vkGetMemoryWin32HandlePropertiesKHR-handleType-00666). La spec impose d'importer avec le même memoryTypeIndex et la même taille qu'à l'export ; le programme vérifie seulement que l'image importée accepte ce type. Non retesté avec les validation layers.
  • Étape 2 mesurée sur un seul GPU et un seul pilote (NVIDIA, Windows). Le mappage d'une image Vulkan OPTIMAL en uchar4 fonctionne ici ; la cohérence après une écriture Vulkan ultérieure à l'import n'est pas testée (elle relève des sémaphores externes, étape 4).
  • La synchronisation repose sur un vkQueueWaitIdle avant l'export : suffisant pour un prototype mono-thread, pas un mécanisme de production. L'étape 4 doit le remplacer par des sémaphores externes, côté Vulkan et côté CUDA.
  • Les deux VkDevice de l'étape 1 vivent dans le même processus. Un partage inter-processus demanderait DuplicateHandle.
  • La vérification deviceUUID de l'étape 1 compare deux requêtes sur le même VkPhysicalDevice : elle est structurellement vraie. L'étape 2 la rend réellement significative en la confrontant à l'UUID du device CUDA.
  • L'aller-retour de référence inclut deux soumissions, deux attentes de file et les copies hôte ; il mesure donc surtout la latence de synchronisation Windows, pas la bande passante PCIe.
  • Les temps export/import sont des mesures uniques par exécution (ils ne peuvent pas être répétés sans réexporter) ; seule la variabilité de l'aller-retour est établie sur 5 essais.
  • Aucune couche de validation Vulkan n'est activée : le prototype n'a pas été passé sous VK_LAYER_KHRONOS_validation.
  • Si les en-têtes CUDA déclenchent des avertissements sous /W4, la cible les inclut en SYSTEM ; le niveau d'avertissement global n'est pas abaissé. Ce point n'a pas pu être vérifié sans Toolkit.

Licences

  • Ce prototype est publié sous MPL 2.0, comme le reste de SEQUA (voir LICENSE à la racine). Chaque fichier source porte l'en-tête SPDX-License-Identifier: MPL-2.0.
  • Dépendances de l'étape 1 : Vulkan SDK / Vulkan-Loader (Apache 2.0) uniquement.
  • Dépendance de l'étape 2 : CUDA Toolkit, propriétaire NVIDIA, distribué sous l'EULA NVIDIA. C'est une dépendance facultative du prototype uniquement : elle n'entre jamais dans le cœur, la cible n'est construite que si CUDAToolkit est trouvé, et l'engine doit rester compilable et utilisable sans CUDA.
  • CUDA et TensorRT sont propriétaires (NVIDIA). Ils n'entreront jamais dans le cœur : ce seront des plugins facultatifs derrière IComputeBackend, conformément au §10.2 du document d'architecture, et ils ne seront pas requis pour compiler ou exécuter l'engine.