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.
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
- Crée une instance Vulkan headless (aucune fenêtre, aucune surface) sur API 1.2.
- Sélectionne un GPU avec file graphique et
VK_KHR_external_memory_win32, puis crée deuxVkDevicedistincts sur le mêmeVkPhysicalDevice, et vérifie que leursdeviceUUIDconcordent. - Alloue une
VkImage1920×1080VK_FORMAT_R8G8B8A8_UNORMen mémoireDEVICE_LOCAL, avecVkExternalMemoryImageCreateInfo+VkExportMemoryAllocateInfo(VK_EXTERNAL_MEMORY_HANDLE_TYPE_OPAQUE_WIN32_BIT) et une allocation dédiée. - 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. - Exporte la mémoire avec
vkGetMemoryWin32HandleKHR, importe leHANDLEdans le second device (VkImportMemoryWin32HandleInfoKHR), lie uneVkImageidentique à la mémoire importée. - Relit l'image du second device via un staging buffer et compare octet à octet au motif.
- 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).
- Ferme le
HANDLEavecCloseHandleet 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).
- Crée l'image Vulkan exportable remplie du motif et exporte son
HANDLEOPAQUE_WIN32. - Énumère les devices CUDA (
cudaGetDeviceCount/cudaGetDeviceProperties) et choisit celui dontcudaDeviceProp::uuidest égal audeviceUUIDVulkan (VkPhysicalDeviceIDProperties). Échec explicite, avec la liste des UUID vus, si aucun ne correspond. - Importe la mémoire avec
cudaImportExternalMemory(cudaExternalMemoryHandleTypeOpaqueWin32) en passantsize =taille de l'allocation Vulkan etcudaExternalMemoryDedicateduniquement parce que l'allocation Vulkan est dédiée (VkMemoryDedicatedAllocateInfodanscreate_filled_exportable_image; le drapeau est propagé depuisExportableImage::dedicated). - Mappe l'image (tiling
OPTIMAL) aveccudaExternalMemoryGetMappedMipmappedArray: format de canaluchar4(8 bits × 4 non signé), extent 1920×1080 (depth 0 = tableau 2D), 1 niveau, puiscudaGetMipmappedArrayLevel(…, 0). - Copie le niveau 0 vers un tampon device avec
cudaMemcpy2DFromArray— c'est le chemin mesuré GPU → GPU :std::chronoautour de l'appel etcudaEventpour le temps GPU. - 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é).
- 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.
- 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:vkGetMemoryWin32HandlePropertiesKHRne doit pas être appelé pour ce type de handle (VUID-vkGetMemoryWin32HandlePropertiesKHR-handleType-00666). La spec impose d'importer avec le mêmememoryTypeIndexet 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
OPTIMALenuchar4fonctionne 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
vkQueueWaitIdleavant 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
VkDevicede l'étape 1 vivent dans le même processus. Un partage inter-processus demanderaitDuplicateHandle. - La vérification
deviceUUIDde l'étape 1 compare deux requêtes sur le mêmeVkPhysicalDevice: 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 enSYSTEM; 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êteSPDX-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
CUDAToolkitest 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.