Navigation : Index | << Précédent | Suivant >>

10e. LLamaSharp : bake-off binding .NET de llama.cpp

Durée estimée : 50 minutes Prérequis : C# asynchrone, base de llama.cpp, runtime .NET 8 self-contained Matériel du run de référence : GPU NVIDIA RTX 3080 Ti 16 Go ; modèle GGUF Qwen3-4B Q4_K_M (~2.5 GB)

Objectifs d’apprentissage

  1. Brancher un binding .NET de llama.cpp (LLamaSharp 0.27.0) sur le même GPU que la Phase 1 (TensorSharp, PR #12645).
  2. Charger un GGUF et mesurer tok/s (débit), VRAM, et taux de jetons <pad> — sans recopier un benchmark tiers.
  3. Comparer à TensorSharp sur les mêmes invites et la même quantification Q4, et statuer sur l’utilité des deux moteurs pour le curriculum.
  4. Diagnostiquer, puis réparer, une fausse détection de backend : pourquoi LLamaSharp 0.27 ignore une carte CUDA parfaitement fonctionnelle, et comment le corriger (règle F : réparer l’environnement, jamais le contourner).

Contexte

Ce notebook est la Phase 2 du bake-off #12353 qui compare trois moteurs d’inférence locale en .NET :

  • TensorSharp (Phase 1, #12645) : serveur HTTP distant sur RTX 3080, mais génération Gemma 4 contenant 159/160 jetons <pad> (RECOVERABLE-LOCAL).
  • LLamaSharp (Phase 2, ce notebook) : binding .NET natif de llama.cpp 0.27.0 — load in-process via P/Invoke, pas de serveur distant.
  • ORT GenAI (Phase 3, à venir) : ONNX Runtime GenAI Microsoft.

Scope partitionné par le coordinateur (ai-01) le 2026-08-24 : MyIA.AI.Notebooks/GenAI/Texte/23_*.ipynb + Texte/README.md (lane myia-po-2026:CoursIA-2, claim posé sur l’issue #12353).

Verdict du pilote : SOTA-OK. LLamaSharp charge Qwen3-4B Q4_K_M et le décode sur la RTX 3080 Ti — 37/37 couches sur CUDA0, deux backends enregistrés, VRAM en hausse de 3163 MiB pendant l’inférence — pour un débit agrégé de 70.11 tok/s sur les quatre invites Phase 1, avec 0 jeton <pad>.

Ce chiffre n’a pas toujours été celui-là, et l’histoire de sa correction est le cœur pédagogique de ce notebook. Le pilote a d’abord mesuré 14.14 tok/s en croyant mesurer un GPU : le backend CUDA ne s’était jamais enregistré et les 353 tokens tombaient sur le CPU, sans erreur ni avertissement. Le débit était authentique, son attribution était fausse. La cause a été remontée jusqu’au bout puis réparée (§1bis). Le facteur ×2.45 que le pilote en tirait comparait en réalité deux journées : le contrôle CPU d’alors venait d’un autre run, à une autre date. C’est précisément l’erreur que le §1quater corrige — le contrôle de la même minute donne ×7,51 (voir ci-dessous).

Ces chiffres sont ceux de l’artefact d’origine (lignée fbe1d5ed…) : le GGUF a depuis changé de lignée (sha256 officiel épinglé en cellule 4ter) et la réponse générée a changé de longueur. Le run de référence committé donne 384 tokens, 70,11 tok/s sur GPU contre 9,34 en contrôle CPU dans la même minute — facteur ×7,51 à charge toujours strictement identique.

Deux leçons transposables : un paramètre demandé n’est pas une mesure (GpuLayerCount = 99 ne prouve rien), et un backend qui ne se charge pas retombe en silence sur le CPU. Le §1ter donne les observables qui tranchent.

import platform, subprocess, json, os

print("=" * 70)
print("Cellule 1 — Pré-flight environnement")
print("=" * 70)
print(f"OS             : {platform.platform()}")
print(f"Architecture   : {platform.machine()}")
print(f"Python         : {platform.python_version()}")

# dotnet SDK + runtimes via --list-runtimes
r = subprocess.run(["dotnet", "--list-runtimes"], capture_output=True, text=True, shell=True)
print("\nRuntimes .NET visibles :")
for line in r.stdout.strip().splitlines():
    print(f"  {line}")
print("\n→ Le runtime .NET 8.0.27 (C:\\dotnet-manual) est requis pour LLamaSharp 0.27.0 (marké compatible net8.0 par NuGet).")
======================================================================
Cellule 1 — Pré-flight environnement
======================================================================
OS             : Windows-10-10.0.26200-SP0
Architecture   : AMD64
Python         : 3.11.9

Runtimes .NET visibles :
  Microsoft.AspNetCore.App 10.0.8 [C:\Program Files\dotnet\shared\Microsoft.AspNetCore.App]
  Microsoft.AspNetCore.App 10.0.12 [C:\Program Files\dotnet\shared\Microsoft.AspNetCore.App]
  Microsoft.NETCore.App 3.1.32 [C:\Program Files\dotnet\shared\Microsoft.NETCore.App]
  Microsoft.NETCore.App 6.0.23 [C:\Program Files\dotnet\shared\Microsoft.NETCore.App]
  Microsoft.NETCore.App 10.0.8 [C:\Program Files\dotnet\shared\Microsoft.NETCore.App]
  Microsoft.NETCore.App 10.0.12 [C:\Program Files\dotnet\shared\Microsoft.NETCore.App]
  Microsoft.WindowsDesktop.App 6.0.23 [C:\Program Files\dotnet\shared\Microsoft.WindowsDesktop.App]
  Microsoft.WindowsDesktop.App 10.0.8 [C:\Program Files\dotnet\shared\Microsoft.WindowsDesktop.App]
  Microsoft.WindowsDesktop.App 10.0.12 [C:\Program Files\dotnet\shared\Microsoft.WindowsDesktop.App]

→ Le runtime .NET 8.0.27 (C:\dotnet-manual) est requis pour LLamaSharp 0.27.0 (marké compatible net8.0 par NuGet).

1. Réparation environnement : installer .NET 8 (règle F)

Pourquoi : LLamaSharp 0.27.0 cible explicitement net8.0 (et netstandard2.0 pour la compatibilité). Sur cette machine, seul .NET 3.1 / 6 / 10 sont installés. L’assembly LLamaSharp.dll charge le binaire natif llama.dll via P/Invoke — le binding natif ne résout pas ses DllImport quand le host est .NET 6 (vérifié empiriquement, TypeLoadException: llama_backend_free has no implementation).

Procédure (sans UAC) :

# Téléchargement du script d'installation officiel
curl -sL -o C:\dev\_scratch\dotnet-install.ps1 https://dot.net/v1/dotnet-install.ps1

# Installation du runtime .NET 8 dans un dossier user-owned (pas de Program Files)
powershell -ExecutionPolicy Bypass -File C:\dev\_scratch\dotnet-install.ps1 \
    -Runtime dotnet -Version 8.0.11 -InstallDir C:\dotnet-manual -NoPath

Le runtime est ensuite résolu manuellement via dotnet publish -r win-x64 --self-contained true qui embarquetoute la BCL + le runtime dans un dossier portable.

Linux / macOS : le script jumeau officiel dotnet-install.sh remplace le .ps1 (mêmes options, en tirets longs) :

curl -sSL https://dot.net/v1/dotnet-install.sh -o /tmp/dotnet-install.sh
chmod +x /tmp/dotnet-install.sh
/tmp/dotnet-install.sh --runtime dotnet --version 8.0.11 --install-dir "$HOME/.dotnet-manual" --no-path

Le publish self-contained suit le même schéma avec le RID de la plateforme : -r linux-x64, -r osx-x64 ou -r osx-arm64 à la place de -r win-x64.

import os
import subprocess
from pathlib import Path

print("=" * 70)
print("Cellule 3 — Publish Test.exe self-contained (.NET 8 + LLamaSharp 0.27.0)")
print("=" * 70)

# La source du harnais est VERSIONNÉE dans le dépôt : tools/llamasharp-bakeoff/
# (test.csproj + Program.cs, reconstruits par #15570). Résolution depuis le dossier
# d'exécution en remontant jusqu'à la série — aucune cellule ne porte de chemin
# machine absolu.
def _trouve_outils():
    for base in [Path.cwd(), *Path.cwd().parents]:
        candidat = base / "tools" / "llamasharp-bakeoff"
        if (candidat / "test.csproj").exists():
            return candidat
    raise FileNotFoundError(
        "tools/llamasharp-bakeoff introuvable — exécuter le notebook depuis la série "
        "GenAI/Texte ou un parent (papermill --cwd la série)")

TOOLS = _trouve_outils()
PUBLISH = TOOLS / "publish"

# LLamaSharp.dll + binaires natifs livrés par NuGet (Backend.Cuda12 → cuda12/avx2)
r = subprocess.run(
    [
        "dotnet", "publish", "test.csproj",
        "-c", "Release",
        "-r", "win-x64",
        "--self-contained", "true",
        "-p:PublishSingleFile=false",
        "-o", str(PUBLISH),
        "--nologo",
    ],
    cwd=str(TOOLS),
    capture_output=True, text=True,
)
print("Restore + Publish :", "OK" if r.returncode == 0 else f"FAILED (exit {r.returncode})")
# Le stdout de MSBuild embarque les chemins absolus du projet : affiché en
# forme relative au harnais (meme discipline que sans_racine en 4bis).
print(r.stdout[-300:].replace(str(TOOLS), "<tools>"))

# Vérification présence des binaires natifs CUDA12
dlls = sorted(os.listdir(PUBLISH))
print("\nArtefacts publiés dans tools/llamasharp-bakeoff/publish :")
for f in [d for d in dlls if d.endswith(".dll") or d.endswith(".exe")]:
    sz = os.path.getsize(os.path.join(PUBLISH, f)) / (1024 * 1024)
    print(f"  {f:60s} {sz:8.2f} MB")
======================================================================
Cellule 3 — Publish Test.exe self-contained (.NET 8 + LLamaSharp 0.27.0)
======================================================================
Restore + Publish : OK
on des projets à restaurer...
  Tous les projets sont à jour pour la restauration.
  test -> <tools>\bin\Release\net8.0\win-x64\Test.dll
  test -> <tools>\publish\


Artefacts publiés dans tools/llamasharp-bakeoff/publish :
  CommunityToolkit.HighPerformance.dll                             0.15 MB
  LLamaSharp.dll                                                   0.28 MB
  Microsoft.Bcl.AsyncInterfaces.dll                                0.02 MB
  Microsoft.Bcl.Memory.dll                                         0.06 MB
  Microsoft.CSharp.dll                                             0.96 MB
  Microsoft.DiaSymReader.Native.amd64.dll                          2.09 MB
  Microsoft.Extensions.AI.Abstractions.dll                         0.64 MB
  Microsoft.Extensions.DependencyInjection.Abstractions.dll        0.06 MB
  Microsoft.Extensions.Logging.Abstractions.dll                    0.06 MB
  Microsoft.VisualBasic.Core.dll                                   1.19 MB
  Microsoft.VisualBasic.dll                                        0.02 MB
  Microsoft.Win32.Primitives.dll                                   0.01 MB
  Microsoft.Win32.Registry.dll                                     0.12 MB
  System.AppContext.dll                                            0.01 MB
  System.Buffers.dll                                               0.01 MB
  System.Collections.Concurrent.dll                                0.26 MB
  System.Collections.Immutable.dll                                 0.80 MB
  System.Collections.NonGeneric.dll                                0.10 MB
  System.Collections.Specialized.dll                               0.10 MB
  System.Collections.dll                                           0.25 MB
  System.ComponentModel.Annotations.dll                            0.19 MB
  System.ComponentModel.DataAnnotations.dll                        0.02 MB
  System.ComponentModel.EventBasedAsync.dll                        0.04 MB
  System.ComponentModel.Primitives.dll                             0.08 MB
  System.ComponentModel.TypeConverter.dll                          0.71 MB
  System.ComponentModel.dll                                        0.03 MB
  System.Configuration.dll                                         0.02 MB
  System.Console.dll                                               0.17 MB
  System.Core.dll                                                  0.02 MB
  System.Data.Common.dll                                           2.73 MB
  System.Data.DataSetExtensions.dll                                0.01 MB
  System.Data.dll                                                  0.02 MB
  System.Diagnostics.Contracts.dll                                 0.02 MB
  System.Diagnostics.Debug.dll                                     0.01 MB
  System.Diagnostics.DiagnosticSource.dll                          0.18 MB
  System.Diagnostics.FileVersionInfo.dll                           0.04 MB
  System.Diagnostics.Process.dll                                   0.32 MB
  System.Diagnostics.StackTrace.dll                                0.04 MB
  System.Diagnostics.TextWriterTraceListener.dll                   0.06 MB
  System.Diagnostics.Tools.dll                                     0.01 MB
  System.Diagnostics.TraceSource.dll                               0.14 MB
  System.Diagnostics.Tracing.dll                                   0.02 MB
  System.Drawing.Primitives.dll                                    0.13 MB
  System.Drawing.dll                                               0.02 MB
  System.Dynamic.Runtime.dll                                       0.02 MB
  System.Formats.Asn1.dll                                          0.23 MB
  System.Formats.Tar.dll                                           0.26 MB
  System.Globalization.Calendars.dll                               0.01 MB
  System.Globalization.Extensions.dll                              0.01 MB
  System.Globalization.dll                                         0.01 MB
  System.IO.Compression.Brotli.dll                                 0.08 MB
  System.IO.Compression.FileSystem.dll                             0.01 MB
  System.IO.Compression.Native.dll                                 0.79 MB
  System.IO.Compression.ZipFile.dll                                0.05 MB
  System.IO.Compression.dll                                        0.25 MB
  System.IO.FileSystem.AccessControl.dll                           0.10 MB
  System.IO.FileSystem.DriveInfo.dll                               0.05 MB
  System.IO.FileSystem.Primitives.dll                              0.01 MB
  System.IO.FileSystem.Watcher.dll                                 0.08 MB
  System.IO.FileSystem.dll                                         0.01 MB
  System.IO.IsolatedStorage.dll                                    0.09 MB
  System.IO.MemoryMappedFiles.dll                                  0.08 MB
  System.IO.Pipelines.dll                                          0.07 MB
  System.IO.Pipes.AccessControl.dll                                0.02 MB
  System.IO.Pipes.dll                                              0.16 MB
  System.IO.UnmanagedMemoryStream.dll                              0.01 MB
  System.IO.dll                                                    0.01 MB
  System.Interactive.Async.dll                                     0.35 MB
  System.Linq.Async.dll                                            1.14 MB
  System.Linq.AsyncEnumerable.dll                                  0.44 MB
  System.Linq.Expressions.dll                                      3.51 MB
  System.Linq.Parallel.dll                                         0.77 MB
  System.Linq.Queryable.dll                                        0.17 MB
  System.Linq.dll                                                  0.52 MB
  System.Memory.dll                                                0.15 MB
  System.Net.Http.Json.dll                                         0.12 MB
  System.Net.Http.dll                                              1.65 MB
  System.Net.HttpListener.dll                                      0.53 MB
  System.Net.Mail.dll                                              0.41 MB
  System.Net.NameResolution.dll                                    0.11 MB
  System.Net.NetworkInformation.dll                                0.15 MB
  System.Net.Ping.dll                                              0.09 MB
  System.Net.Primitives.dll                                        0.22 MB
  System.Net.Quic.dll                                              0.27 MB
  System.Net.Requests.dll                                          0.33 MB
  System.Net.Security.dll                                          0.64 MB
  System.Net.ServicePoint.dll                                      0.04 MB
  System.Net.Sockets.dll                                           0.52 MB
  System.Net.WebClient.dll                                         0.16 MB
  System.Net.WebHeaderCollection.dll                               0.06 MB
  System.Net.WebProxy.dll                                          0.04 MB
  System.Net.WebSockets.Client.dll                                 0.10 MB
  System.Net.WebSockets.dll                                        0.18 MB
  System.Net.dll                                                   0.02 MB
  System.Numerics.Tensors.dll                                      0.52 MB
  System.Numerics.Vectors.dll                                      0.01 MB
  System.Numerics.dll                                              0.01 MB
  System.ObjectModel.dll                                           0.08 MB
  System.Private.CoreLib.dll                                      12.56 MB
  System.Private.DataContractSerialization.dll                     1.99 MB
  System.Private.Uri.dll                                           0.25 MB
  System.Private.Xml.Linq.dll                                      0.38 MB
  System.Private.Xml.dll                                           7.63 MB
  System.Reflection.DispatchProxy.dll                              0.07 MB
  System.Reflection.Emit.ILGeneration.dll                          0.01 MB
  System.Reflection.Emit.Lightweight.dll                           0.01 MB
  System.Reflection.Emit.dll                                       0.12 MB
  System.Reflection.Extensions.dll                                 0.01 MB
  System.Reflection.Metadata.dll                                   1.06 MB
  System.Reflection.Primitives.dll                                 0.01 MB
  System.Reflection.TypeExtensions.dll                             0.04 MB
  System.Reflection.dll                                            0.02 MB
  System.Resources.Reader.dll                                      0.01 MB
  System.Resources.ResourceManager.dll                             0.01 MB
  System.Resources.Writer.dll                                      0.05 MB
  System.Runtime.CompilerServices.Unsafe.dll                       0.01 MB
  System.Runtime.CompilerServices.VisualC.dll                      0.03 MB
  System.Runtime.Extensions.dll                                    0.02 MB
  System.Runtime.Handles.dll                                       0.01 MB
  System.Runtime.InteropServices.JavaScript.dll                    0.05 MB
  System.Runtime.InteropServices.RuntimeInformation.dll            0.01 MB
  System.Runtime.InteropServices.dll                               0.09 MB
  System.Runtime.Intrinsics.dll                                    0.02 MB
  System.Runtime.Loader.dll                                        0.01 MB
  System.Runtime.Numerics.dll                                      0.31 MB
  System.Runtime.Serialization.Formatters.dll                      0.29 MB
  System.Runtime.Serialization.Json.dll                            0.01 MB
  System.Runtime.Serialization.Primitives.dll                      0.04 MB
  System.Runtime.Serialization.Xml.dll                             0.02 MB
  System.Runtime.Serialization.dll                                 0.02 MB
  System.Runtime.dll                                               0.04 MB
  System.Security.AccessControl.dll                                0.22 MB
  System.Security.Claims.dll                                       0.10 MB
  System.Security.Cryptography.Algorithms.dll                      0.02 MB
  System.Security.Cryptography.Cng.dll                             0.02 MB
  System.Security.Cryptography.Csp.dll                             0.01 MB
  System.Security.Cryptography.Encoding.dll                        0.01 MB
  System.Security.Cryptography.OpenSsl.dll                         0.01 MB
  System.Security.Cryptography.Primitives.dll                      0.01 MB
  System.Security.Cryptography.X509Certificates.dll                0.02 MB
  System.Security.Cryptography.dll                                 1.95 MB
  System.Security.Principal.Windows.dll                            0.18 MB
  System.Security.Principal.dll                                    0.01 MB
  System.Security.SecureString.dll                                 0.01 MB
  System.Security.dll                                              0.02 MB
  System.ServiceModel.Web.dll                                      0.02 MB
  System.ServiceProcess.dll                                        0.01 MB
  System.Text.Encoding.CodePages.dll                               0.82 MB
  System.Text.Encoding.Extensions.dll                              0.01 MB
  System.Text.Encoding.dll                                         0.01 MB
  System.Text.Encodings.Web.dll                                    0.06 MB
  System.Text.Json.dll                                             0.65 MB
  System.Text.RegularExpressions.dll                               0.97 MB
  System.Threading.Channels.dll                                    0.13 MB
  System.Threading.Overlapped.dll                                  0.01 MB
  System.Threading.Tasks.Dataflow.dll                              0.47 MB
  System.Threading.Tasks.Extensions.dll                            0.01 MB
  System.Threading.Tasks.Parallel.dll                              0.13 MB
  System.Threading.Tasks.dll                                       0.02 MB
  System.Threading.Thread.dll                                      0.01 MB
  System.Threading.ThreadPool.dll                                  0.01 MB
  System.Threading.Timer.dll                                       0.01 MB
  System.Threading.dll                                             0.08 MB
  System.Transactions.Local.dll                                    0.63 MB
  System.Transactions.dll                                          0.02 MB
  System.ValueTuple.dll                                            0.01 MB
  System.Web.HttpUtility.dll                                       0.06 MB
  System.Web.dll                                                   0.01 MB
  System.Windows.dll                                               0.01 MB
  System.Xml.Linq.dll                                              0.01 MB
  System.Xml.ReaderWriter.dll                                      0.02 MB
  System.Xml.Serialization.dll                                     0.02 MB
  System.Xml.XDocument.dll                                         0.01 MB
  System.Xml.XPath.XDocument.dll                                   0.03 MB
  System.Xml.XPath.dll                                             0.01 MB
  System.Xml.XmlDocument.dll                                       0.01 MB
  System.Xml.XmlSerializer.dll                                     0.02 MB
  System.Xml.dll                                                   0.02 MB
  System.dll                                                       0.05 MB
  Test.dll                                                         0.01 MB
  Test.exe                                                         0.14 MB
  WindowsBase.dll                                                  0.02 MB
  clretwrc.dll                                                     0.30 MB
  clrgc.dll                                                        0.64 MB
  clrjit.dll                                                       1.70 MB
  coreclr.dll                                                      4.76 MB
  createdump.exe                                                   0.07 MB
  cublas64_12.dll                                                 95.40 MB
  cublasLt64_12.dll                                              451.61 MB
  cudart64_12.dll                                                  0.53 MB
  hostfxr.dll                                                      0.35 MB
  hostpolicy.dll                                                   0.39 MB
  mscordaccore.dll                                                 1.28 MB
  mscordaccore_amd64_amd64_8.0.2726.22922.dll                      1.28 MB
  mscordbi.dll                                                     1.18 MB
  mscorlib.dll                                                     0.06 MB
  mscorrc.dll                                                      0.13 MB
  msquic.dll                                                       0.50 MB
  netstandard.dll                                                  0.10 MB
import subprocess

print("=" * 70)
print("Cellule 4 — GPU + VRAM avant lancement")
print("=" * 70)
r = subprocess.run(
    ["nvidia-smi", "--query-gpu=name,memory.total,memory.used,memory.free,driver_version",
     "--format=csv,noheader"],
    capture_output=True, text=True,
)
print(r.stdout)
free = int(r.stdout.split(",")[3].strip().split()[0])
print(f"→ VRAM libre relue dans la sortie : {free} MiB "
      f"({'suffisant' if free > 3500 else 'INSUFFISANT'} pour Qwen3-4B Q4_K_M, ~2.5 GB + KV cache).")
======================================================================
Cellule 4 — GPU + VRAM avant lancement
======================================================================
NVIDIA GeForce RTX 3080 Ti Laptop GPU, 16384 MiB, 7413 MiB, 8761 MiB, 616.56

→ VRAM libre relue dans la sortie : 8761 MiB (suffisant pour Qwen3-4B Q4_K_M, ~2.5 GB + KV cache).

1bis. Réparer la détection CUDA avant de mesurer (règle F)

Sur cette machine, LLamaSharp 0.27 refuse le GPU alors que la carte, le pilote et le runtime CUDA sont tous présents. Deux défauts se superposent — l’un de détection, l’autre de résolution de dépendances — et chacun est silencieux.

Défaut 1 — la détection interroge le toolkit, pas le pilote. Le sélecteur de bibliothèque native lit %CUDA_PATH%/version.json et y cherche la clé libcublas pour en déduire une version majeure. Sans Toolkit installé, CudaMajorVersion vaut -1 et le candidat cuda12 n’est jamais énuméré — alors même que le dump de configuration affiche PreferCuda: True. C’est un faux négatif : le runtime CUDA, lui, est bien là, livré par le paquet LLamaSharp.Backend.Cuda12.Windows. Conséquence directe : NativeLibraryConfig.All.WithCuda(true) ne répare rien, puisque le drapeau était déjà à true — ce qui manquait n’était pas l’intention, c’était la sonde.

Défaut 2 — les dépendances se résolvent depuis le dossier de la DLL. Une fois le candidat énuméré, ggml-cuda.dll est chargé par chemin absolu ; Windows cherche alors ses dépendances dans son répertoire, pas dans celui de l’exécutable. Les cudart64_12.dll / cublas64_12.dll / cublasLt64_12.dll posées à la racine du publish lui sont donc invisibles, et l’échec en cascade (ggml-cuda → ggml → llama) fait retomber le loader sur le candidat suivant, avx2 — le CPU.

Trois voies, dont une qui ne marche pas

Voie Statut Note
(a) Installer le CUDA Toolkit propre La voie officielle : version.json devient réel. Lourd (~3 Go) pour un besoin de détection.
(b) Sonde CUDA_PATH + colocalisation des DLL mesurée ici Ce que fait la cellule ci-dessous. Aucune compilation CUDA n’est requise — seulement satisfaire l’heuristique.
(c) NativeLibraryConfig.WithLibrary(<chemin>) mesurée insuffisante L’API « désigne le fichier natif » construit une liste générique ggml-base → ggml → llama et ne charge aucun backend : ggml.dll échoue, puis llama.dll. Résultat négatif utile — la réponse évidente ne fonctionne pas.

Honnêteté sur la voie (b) : le version.json écrit ci-dessous est une sonde de détection, pas un faux Toolkit. Rien n’est compilé, aucun nvcc n’est invoqué ; on renseigne la seule valeur que LLamaSharp va lire, pour qu’il cesse d’écarter à tort un backend réellement présent. Un lecteur qui préfère la voie (a) obtiendra le même résultat par un chemin plus lourd.

Verdict SOTA : RECOVERABLE-LOCAL — réparé sur la machine du worker, sans action user.

import glob
import json
import os
import shutil
import subprocess
import sys
import zipfile
from pathlib import Path

print("=" * 70)
print("Cellule 4bis — Réparer la détection CUDA de LLamaSharp (règle F)")
print("=" * 70)

# Chemins relatifs au harnais versionné (tools/llamasharp-bakeoff/) : une sortie
# commitée ne porte aucun chemin machine, le préfixe est lisible ici dans la source.
NATIVE = PUBLISH / "runtimes/win-x64/native/cuda12"
PROBE = TOOLS / "cuda-detect-probe"
CUDA_LIBS = TOOLS / ".cuda-libs"


def sans_racine(chemin):
    """Chemin relatif au harnais, préfixe `<tools>/` (séparateurs POSIX)."""
    return "<tools>/" + Path(chemin).relative_to(TOOLS).as_posix()


# --- Constat : la détection s'appuie sur le TOOLKIT, absent ici ---------------
print(f"CUDA_PATH avant : {os.environ.get('CUDA_PATH', '(non défini)')}")
print(f"nvcc            : {shutil.which('nvcc') or '(absent — pas de Toolkit)'}")
print("→ CudaMajorVersion = -1, donc le candidat cuda12 n'est jamais énuméré,")
print("  bien que la carte, le pilote et un runtime CUDA chargeable soient là.")

# --- Défaut 1 : sonde de détection (la clé lue est libcublas, PAS cuda) -------
(PROBE / "bin").mkdir(parents=True, exist_ok=True)
(PROBE / "version.json").write_text(
    json.dumps(
        {
            "cuda": {"name": "CUDA SDK", "version": "12.4.0"},
            "libcublas": {"name": "CUDA cuBLAS", "version": "12.4.2.65"},
        },
        indent=3,
    ),
    encoding="utf-8",
)
os.environ["CUDA_PATH"] = str(PROBE)
print(f"\n[1/2] Sonde écrite : {sans_racine(PROBE / 'version.json')}  (clé lue : libcublas)")

# --- Défaut 2 : colocaliser le runtime CUDA auprès de ggml-cuda.dll -----------
# Mesuré (#15570) : le paquet Backend.Cuda12.Windows ne livre que ggml-base/ggml/
# ggml-cuda/llama/mtmd — PAS cudart/cublas. Ces trois DLL viennent des wheels
# NVIDIA cu12 (canal de redistribution officiel, sans Toolkit ni UAC). La
# dépendance est TRANSITIVE : cublas64_12 importe cublasLt64_12 — deux DLL ne
# suffisent pas, il en faut trois.
NEEDED = ("cudart64_12.dll", "cublas64_12.dll", "cublasLt64_12.dll")

if not all((NATIVE / d).exists() for d in NEEDED):
    CUDA_LIBS.mkdir(exist_ok=True)
    r = subprocess.run(
        [sys.executable, "-m", "pip", "download", "--only-binary=:all:", "--no-deps",
         "-q", "-d", str(CUDA_LIBS),
         "nvidia-cuda-runtime-cu12==12.4.127", "nvidia-cublas-cu12==12.4.5.8"],
        capture_output=True, text=True)
    print(f"      Wheels cu12 → {sans_racine(CUDA_LIBS)} : "
          f"{'OK' if r.returncode == 0 else f'FAILED (exit {r.returncode})'}")
    for whl in glob.glob(str(CUDA_LIBS / "*.whl")):
        with zipfile.ZipFile(whl) as z:
            for n in z.namelist():
                if n.endswith(".dll") and Path(n).name in NEEDED:
                    (PUBLISH / Path(n).name).write_bytes(z.read(n))

copied, already = [], []
for dll in NEEDED:
    source, target = PUBLISH / dll, NATIVE / dll
    if target.exists():
        already.append(dll)
    elif source.exists():
        shutil.copy2(source, target)
        copied.append(dll)
print(f"[2/2] Runtime CUDA auprès de ggml-cuda.dll — copiés : {copied or 'aucun'} ; "
      f"déjà en place : {already or 'aucun'}")

# --- Vérification OBSERVABLE (pas une déduction) ------------------------------
print("\n--- Contenu du candidat cuda12 (tailles en octets) ---")
for f in sorted(NATIVE.glob("*.dll")):
    print(f"  {f.name:24s} {f.stat().st_size:>12,}")
======================================================================
Cellule 4bis — Réparer la détection CUDA de LLamaSharp (règle F)
======================================================================
CUDA_PATH avant : (non défini)
nvcc            : (absent — pas de Toolkit)
→ CudaMajorVersion = -1, donc le candidat cuda12 n'est jamais énuméré,
  bien que la carte, le pilote et un runtime CUDA chargeable soient là.

[1/2] Sonde écrite : <tools>/cuda-detect-probe/version.json  (clé lue : libcublas)
[2/2] Runtime CUDA auprès de ggml-cuda.dll — copiés : aucun ; déjà en place : ['cudart64_12.dll', 'cublas64_12.dll', 'cublasLt64_12.dll']

--- Contenu du candidat cuda12 (tailles en octets) ---
  cublas64_12.dll           100,033,536
  cublasLt64_12.dll         473,551,360
  cudart64_12.dll               553,984
  ggml-base.dll                 615,936
  ggml-cuda.dll             526,254,592
  ggml.dll                       67,584
  llama.dll                   2,055,680
  mtmd.dll                      802,816
import hashlib
import subprocess
from pathlib import Path

print("=" * 70)
print("Cellule 4ter — Acquisition du modèle GGUF (machine propre)")
print("=" * 70)

# Machine propre = le GGUF n'est pas dans le dépôt (2,4 Go, gitignoré). La source
# officielle Qwen sert de pin : le sha256 contrôlé atteste la lignée du fichier.
MODEL = TOOLS / "models" / "Qwen3-4B-Q4_K_M.gguf"
SHA_ATTENDU = "7485fe6f11af29433bc51cab58009521"  # 32 premiers caractères hex

if not MODEL.exists():
    MODEL.parent.mkdir(parents=True, exist_ok=True)
    print("Téléchargement (2,4 Go, Qwen/Qwen3-4B-GGUF)…")
    r = subprocess.run(
        ["curl", "-sL", "-C", "-", "--retry", "5", "--retry-delay", "5",
         "-o", str(MODEL),
         "https://huggingface.co/Qwen/Qwen3-4B-GGUF/resolve/main/Qwen3-4B-Q4_K_M.gguf"],
        capture_output=True, text=True)
    print("curl :", "OK" if r.returncode == 0 else f"FAILED (exit {r.returncode})")

h = hashlib.sha256()
with open(MODEL, "rb") as f:
    for chunk in iter(lambda: f.read(1 << 20), b""):
        h.update(chunk)
sha32 = h.hexdigest()[:32]
verdict = "VÉRIFIÉ (Qwen3-4B Q4_K_M officiel)" if sha32 == SHA_ATTENDU else \
    f"DIFFÉRENT de l'attendu {SHA_ATTENDU} — ne pas mesurer sans comprendre"
print(f"GGUF   : <tools>/{MODEL.relative_to(TOOLS).as_posix()}")
print(f"Taille : {MODEL.stat().st_size / 1024 / 1024:.2f} MiB")
print(f"SHA256 : {sha32}... — {verdict}")
======================================================================
Cellule 4ter — Acquisition du modèle GGUF (machine propre)
======================================================================
GGUF   : <tools>/models/Qwen3-4B-Q4_K_M.gguf
Taille : 2381.59 MiB
SHA256 : 7485fe6f11af29433bc51cab58009521... — VÉRIFIÉ (Qwen3-4B Q4_K_M officiel)
import subprocess, hashlib, os, threading, time

def digest_natif(txt, discriminants=("assigned to device", "offloaded", "backend_ptrs",
                                     "compute buffer", "graph splits", "load_tensors")):
    """Rend un condense LISIBLE du log natif, sans rien dissimuler.

    Le log brut est domine par des lignes `CUDA Graph id N reused` repetees des
    dizaines de fois, qui noient exactement les lignes qui disent OU le decodage
    a eu lieu. Un simple `txt[-2400:]` est pire que rien ici : il coupe en plein
    mot ET il jette le debut du log, c'est-a-dire la totalite des observables
    discriminants. On garde donc ces derniers integralement, on replie les
    repetitions, et on ANNONCE le nombre de lignes repliees a l'endroit ou elles
    se trouvaient : un condense qui masquerait son propre taux de compression ne
    vaudrait pas mieux qu'une sortie tronquee au hasard.
    """
    sortie, repliees = [], 0
    for ligne in (txt or "").splitlines():
        garder = (not ligne.startswith("[native/")) or any(m in ligne for m in discriminants)
        if garder:
            if repliees:
                sortie.append(f"[... {repliees} lignes de trace native repetitives repliees ...]")
                repliees = 0
            sortie.append(ligne)
        else:
            repliees += 1
    if repliees:
        sortie.append(f"[... {repliees} lignes de trace native repetitives repliees ...]")
    return "\n".join(sortie)


print("=" * 70)
print("Cellule 5 — Smoke run : 1 invite simple + échantillonnage VRAM")
print("=" * 70)

# Forme RELATIVE passée à Test.exe : sa bannière répète l'argument tel quel —
# une sortie committée ne porte aucun chemin machine. L'accès fichier côté
# kernel passe par la forme absolue, elle, jamais imprimée.
gguf = MODEL.relative_to(TOOLS).as_posix()  # acquis et vérifié en cellule 4ter
gguf_abs = TOOLS / gguf
print(f"GGUF         : <tools>/{gguf}")
print(f"Size         : {os.path.getsize(gguf_abs) / 1024 / 1024:.2f} MiB")
sha = hashlib.sha256()
with open(gguf_abs, "rb") as f:
    for chunk in iter(lambda: f.read(1 << 20), b""):
        sha.update(chunk)
print(f"SHA256       : {sha.hexdigest()[:32]}... (32 premiers caractères)")

# La VRAM échantillonnée PENDANT l'inférence est l'observable le moins coûteux et le
# plus difficile à contrefaire : si elle ne bouge pas d'un MiB pendant que le modèle
# décode, rien ne s'exécute sur le GPU. C'est exactement ce qui avait trahi le décodage
# CPU du run initial (min = max sur ~100 échantillons).
vram_samples, sampler_errors = [], []
stop = threading.Event()


def _sample_vram():
    while not stop.is_set():
        try:
            out = subprocess.run(
                ["nvidia-smi", "--query-gpu=memory.used", "--format=csv,noheader,nounits"],
                capture_output=True, text=True, timeout=5,
            ).stdout.strip().splitlines()
            if out:
                vram_samples.append(int(out[0]))
        except Exception as exc:
            sampler_errors.append(str(exc))
        time.sleep(0.2)


sampler = threading.Thread(target=_sample_vram, daemon=True)
sampler.start()

# Exécution RÉELLE du Test.exe self-contained — pas une sortie collée.
# La cellule 4bis a réparé la détection CUDA : le décodage a donc lieu sur GPU.
r = subprocess.run(
    [str(PUBLISH / "Test.exe"), gguf, "96", "smoke"],
    capture_output=True, text=True,
    cwd=str(TOOLS),  # le chemin gguf est relatif au harnais
    timeout=180,
)
stop.set()
sampler.join(timeout=3)
smoke_out = r.stdout

if vram_samples:
    lo, hi = min(vram_samples), max(vram_samples)
    print(f"\nVRAM pendant l'inférence : min {lo} MiB / max {hi} MiB "
          f"/ delta {hi - lo} MiB ({len(vram_samples)} échantillons)")
    print("  → un delta nul signerait un décodage CPU ; ici il est franc.")
else:
    print(f"\nVRAM : aucun échantillon ({len(sampler_errors)} erreurs de sonde) — "
          "observable indisponible, ne rien en conclure.")

print("\n--- Sortie Test.exe (exécution live, trace native condensée) ---")
print(digest_natif(smoke_out))
======================================================================
Cellule 5 — Smoke run : 1 invite simple + échantillonnage VRAM
======================================================================
GGUF         : <tools>/models/Qwen3-4B-Q4_K_M.gguf
Size         : 2381.59 MiB
SHA256       : 7485fe6f11af29433bc51cab58009521... (32 premiers caractères)

VRAM pendant l'inférence : min 7413 MiB / max 10576 MiB / delta 3163 MiB (27 échantillons)
  → un delta nul signerait un décodage CPU ; ici il est franc.

--- Sortie Test.exe (exécution live, trace native condensée) ---
=== LLamaSharp 0.27.0 Bake-Off Qwen3-4B Q4_K_M ===
Date UTC           : 2026-09-11T08:04:57Z
Host .NET          : .NET 8.0.27
Assembly LLamaSharp: LLamaSharp, Version=0.0.0.0, Culture=neutral, PublicKeyToken=null
[... 6 lignes de trace native repetitives repliees ...]
- LibraryName: LLama
- Path: ''
- PreferCuda: True
- PreferVulkan: True
- PreferredAvxLevel: AVX2
- AllowFallback: True
- SkipCheck: False
- SearchDirectories and Priorities: { ./ }
[... 1 lignes de trace native repetitives repliees ...]
- LibraryName: LLama
- Path: ''
- PreferCuda: True
- PreferVulkan: True
- PreferredAvxLevel: AVX2
- AllowFallback: True
- SkipCheck: False
- SearchDirectories and Priorities: { ./ }
[... 15 lignes de trace native repetitives repliees ...]
llama_max_devices  : 16
GGUF path          : models/Qwen3-4B-Q4_K_M.gguf
GGUF size          : 2381 MiB
GGUF sha256        : 7485fe6f11af29433bc51cab58009521... (truncated)
[... 127 lignes de trace native repetitives repliees ...]
[native/Info] load_tensors: loading model tensors, this can take a while... (mmap = true, direct_io = false)
[native/Debug] load_tensors: layer   0 assigned to device CUDA0, is_swa = 0
[native/Debug] load_tensors: layer   1 assigned to device CUDA0, is_swa = 0
[native/Debug] load_tensors: layer   2 assigned to device CUDA0, is_swa = 0
[native/Debug] load_tensors: layer   3 assigned to device CUDA0, is_swa = 0
[native/Debug] load_tensors: layer   4 assigned to device CUDA0, is_swa = 0
[native/Debug] load_tensors: layer   5 assigned to device CUDA0, is_swa = 0
[native/Debug] load_tensors: layer   6 assigned to device CUDA0, is_swa = 0
[native/Debug] load_tensors: layer   7 assigned to device CUDA0, is_swa = 0
[native/Debug] load_tensors: layer   8 assigned to device CUDA0, is_swa = 0
[native/Debug] load_tensors: layer   9 assigned to device CUDA0, is_swa = 0
[native/Debug] load_tensors: layer  10 assigned to device CUDA0, is_swa = 0
[native/Debug] load_tensors: layer  11 assigned to device CUDA0, is_swa = 0
[native/Debug] load_tensors: layer  12 assigned to device CUDA0, is_swa = 0
[native/Debug] load_tensors: layer  13 assigned to device CUDA0, is_swa = 0
[native/Debug] load_tensors: layer  14 assigned to device CUDA0, is_swa = 0
[native/Debug] load_tensors: layer  15 assigned to device CUDA0, is_swa = 0
[native/Debug] load_tensors: layer  16 assigned to device CUDA0, is_swa = 0
[native/Debug] load_tensors: layer  17 assigned to device CUDA0, is_swa = 0
[native/Debug] load_tensors: layer  18 assigned to device CUDA0, is_swa = 0
[native/Debug] load_tensors: layer  19 assigned to device CUDA0, is_swa = 0
[native/Debug] load_tensors: layer  20 assigned to device CUDA0, is_swa = 0
[native/Debug] load_tensors: layer  21 assigned to device CUDA0, is_swa = 0
[native/Debug] load_tensors: layer  22 assigned to device CUDA0, is_swa = 0
[native/Debug] load_tensors: layer  23 assigned to device CUDA0, is_swa = 0
[native/Debug] load_tensors: layer  24 assigned to device CUDA0, is_swa = 0
[native/Debug] load_tensors: layer  25 assigned to device CUDA0, is_swa = 0
[native/Debug] load_tensors: layer  26 assigned to device CUDA0, is_swa = 0
[native/Debug] load_tensors: layer  27 assigned to device CUDA0, is_swa = 0
[native/Debug] load_tensors: layer  28 assigned to device CUDA0, is_swa = 0
[native/Debug] load_tensors: layer  29 assigned to device CUDA0, is_swa = 0
[native/Debug] load_tensors: layer  30 assigned to device CUDA0, is_swa = 0
[native/Debug] load_tensors: layer  31 assigned to device CUDA0, is_swa = 0
[native/Debug] load_tensors: layer  32 assigned to device CUDA0, is_swa = 0
[native/Debug] load_tensors: layer  33 assigned to device CUDA0, is_swa = 0
[native/Debug] load_tensors: layer  34 assigned to device CUDA0, is_swa = 0
[native/Debug] load_tensors: layer  35 assigned to device CUDA0, is_swa = 0
[native/Debug] load_tensors: layer  36 assigned to device CUDA0, is_swa = 0
[... 400 lignes de trace native repetitives repliees ...]
[native/Info] load_tensors: offloading output layer to GPU
[native/Info] load_tensors: offloading 35 repeating layers to GPU
[native/Info] load_tensors: offloaded 37/37 layers to GPU
[native/Info] load_tensors:   CPU_Mapped model buffer size =   304.28 MiB
[native/Info] load_tensors:        CUDA0 model buffer size =  2375.91 MiB
[... 81 lignes de trace native repetitives repliees ...]
GGUF load elapsed   : 1,84 s
Context ready       : n_ctx=2048, n_gpu_layers=99
[... 55 lignes de trace native repetitives repliees ...]
[native/Debug] llama_context: backend_ptrs.size() = 2
[... 14 lignes de trace native repetitives repliees ...]
[native/Info] sched_reserve:      CUDA0 compute buffer size =   301.75 MiB
[native/Info] sched_reserve:  CUDA_Host compute buffer size =    14.01 MiB
[... 1 lignes de trace native repetitives repliees ...]
[native/Info] sched_reserve: graph splits = 2
[... 96 lignes de trace native repetitives repliees ...]
--- Requête 1 ---
Prompt: Definis GGUF en deux phrases.
Tokens: 96, Pad: 0, Time: 0,84s, Rate: 114,84 tok/s
Output complet:
 GGUF est un format de fichier utilisé pour stocker des modèles de langage, notamment dans le domaine de l'intelligence artificielle, permettant de charger et d'utiliser ces modèles de manière efficace. Il est conçu pour être compatible avec diverses bibliothèques et outils de traitement du langage naturel.
GGUF est un format de fichier standardisé pour les modèles de langage, développé pour améliorer l
--- Fin requête 1 ---
=== Résumé agrégé ===
Requests          : 1
Total tokens brut : 96
Total <pad> jetons: 0
Time total        : 0,84 s
Rate agrégé brut  : 114,84 tok/s
Pad ratio         : 0,00%
[native/Debug] ~llama_context:      CUDA0 compute buffer size is 301.7500 MiB, matches expectation of 301.7500 MiB
[native/Debug] ~llama_context:  CUDA_Host compute buffer size is  14.0137 MiB, matches expectation of  14.0137 MiB
import subprocess

print("=" * 70)
print("Cellule 6 — Batch run : 4 invites identiques à Phase 1 (#12645)")
print("=" * 70)

# Forme relative au harnais, exécutée avec cwd=TOOLS (cf. cellule 5) : la
# bannière de Test.exe répète l'argument tel quel — pas de chemin machine.
gguf = MODEL.relative_to(TOOLS).as_posix()  # acquis et vérifié en cellule 4ter

# Exécution RÉELLE — 4 invites, identiques à Phase 1. Sampling déterministe
# (Temperature = 0.0) : le nombre de tokens est reproductible d'un run à l'autre,
# ce qui rend la comparaison CPU/GPU exacte à tokens égaux.
r = subprocess.run(
    [str(PUBLISH / "Test.exe"), gguf, "96", "batch"],
    capture_output=True, text=True,
    cwd=str(TOOLS),  # le chemin gguf est relatif au harnais
    timeout=180,
)
batch_out = r.stdout

# `digest_natif` est defini en cellule 5 (execution sequentielle du notebook).
print("--- Sortie Test.exe batch (exécution live, trace native condensée) ---")
print(digest_natif(batch_out))
======================================================================
Cellule 6 — Batch run : 4 invites identiques à Phase 1 (#12645)
======================================================================
--- Sortie Test.exe batch (exécution live, trace native condensée) ---
=== LLamaSharp 0.27.0 Bake-Off Qwen3-4B Q4_K_M ===
Date UTC           : 2026-09-11T08:05:04Z
Host .NET          : .NET 8.0.27
Assembly LLamaSharp: LLamaSharp, Version=0.0.0.0, Culture=neutral, PublicKeyToken=null
[... 6 lignes de trace native repetitives repliees ...]
- LibraryName: LLama
- Path: ''
- PreferCuda: True
- PreferVulkan: True
- PreferredAvxLevel: AVX2
- AllowFallback: True
- SkipCheck: False
- SearchDirectories and Priorities: { ./ }
[... 1 lignes de trace native repetitives repliees ...]
- LibraryName: LLama
- Path: ''
- PreferCuda: True
- PreferVulkan: True
- PreferredAvxLevel: AVX2
- AllowFallback: True
- SkipCheck: False
- SearchDirectories and Priorities: { ./ }
[... 15 lignes de trace native repetitives repliees ...]
llama_max_devices  : 16
GGUF path          : models/Qwen3-4B-Q4_K_M.gguf
GGUF size          : 2381 MiB
GGUF sha256        : 7485fe6f11af29433bc51cab58009521... (truncated)
[... 127 lignes de trace native repetitives repliees ...]
[native/Info] load_tensors: loading model tensors, this can take a while... (mmap = true, direct_io = false)
[native/Debug] load_tensors: layer   0 assigned to device CUDA0, is_swa = 0
[native/Debug] load_tensors: layer   1 assigned to device CUDA0, is_swa = 0
[native/Debug] load_tensors: layer   2 assigned to device CUDA0, is_swa = 0
[native/Debug] load_tensors: layer   3 assigned to device CUDA0, is_swa = 0
[native/Debug] load_tensors: layer   4 assigned to device CUDA0, is_swa = 0
[native/Debug] load_tensors: layer   5 assigned to device CUDA0, is_swa = 0
[native/Debug] load_tensors: layer   6 assigned to device CUDA0, is_swa = 0
[native/Debug] load_tensors: layer   7 assigned to device CUDA0, is_swa = 0
[native/Debug] load_tensors: layer   8 assigned to device CUDA0, is_swa = 0
[native/Debug] load_tensors: layer   9 assigned to device CUDA0, is_swa = 0
[native/Debug] load_tensors: layer  10 assigned to device CUDA0, is_swa = 0
[native/Debug] load_tensors: layer  11 assigned to device CUDA0, is_swa = 0
[native/Debug] load_tensors: layer  12 assigned to device CUDA0, is_swa = 0
[native/Debug] load_tensors: layer  13 assigned to device CUDA0, is_swa = 0
[native/Debug] load_tensors: layer  14 assigned to device CUDA0, is_swa = 0
[native/Debug] load_tensors: layer  15 assigned to device CUDA0, is_swa = 0
[native/Debug] load_tensors: layer  16 assigned to device CUDA0, is_swa = 0
[native/Debug] load_tensors: layer  17 assigned to device CUDA0, is_swa = 0
[native/Debug] load_tensors: layer  18 assigned to device CUDA0, is_swa = 0
[native/Debug] load_tensors: layer  19 assigned to device CUDA0, is_swa = 0
[native/Debug] load_tensors: layer  20 assigned to device CUDA0, is_swa = 0
[native/Debug] load_tensors: layer  21 assigned to device CUDA0, is_swa = 0
[native/Debug] load_tensors: layer  22 assigned to device CUDA0, is_swa = 0
[native/Debug] load_tensors: layer  23 assigned to device CUDA0, is_swa = 0
[native/Debug] load_tensors: layer  24 assigned to device CUDA0, is_swa = 0
[native/Debug] load_tensors: layer  25 assigned to device CUDA0, is_swa = 0
[native/Debug] load_tensors: layer  26 assigned to device CUDA0, is_swa = 0
[native/Debug] load_tensors: layer  27 assigned to device CUDA0, is_swa = 0
[native/Debug] load_tensors: layer  28 assigned to device CUDA0, is_swa = 0
[native/Debug] load_tensors: layer  29 assigned to device CUDA0, is_swa = 0
[native/Debug] load_tensors: layer  30 assigned to device CUDA0, is_swa = 0
[native/Debug] load_tensors: layer  31 assigned to device CUDA0, is_swa = 0
[native/Debug] load_tensors: layer  32 assigned to device CUDA0, is_swa = 0
[native/Debug] load_tensors: layer  33 assigned to device CUDA0, is_swa = 0
[native/Debug] load_tensors: layer  34 assigned to device CUDA0, is_swa = 0
[native/Debug] load_tensors: layer  35 assigned to device CUDA0, is_swa = 0
[native/Debug] load_tensors: layer  36 assigned to device CUDA0, is_swa = 0
[... 400 lignes de trace native repetitives repliees ...]
[native/Info] load_tensors: offloading output layer to GPU
[native/Info] load_tensors: offloading 35 repeating layers to GPU
[native/Info] load_tensors: offloaded 37/37 layers to GPU
[native/Info] load_tensors:   CPU_Mapped model buffer size =   304.28 MiB
[native/Info] load_tensors:        CUDA0 model buffer size =  2375.91 MiB
[... 81 lignes de trace native repetitives repliees ...]
GGUF load elapsed   : 1,71 s
Context ready       : n_ctx=2048, n_gpu_layers=99
[... 55 lignes de trace native repetitives repliees ...]
[native/Debug] llama_context: backend_ptrs.size() = 2
[... 14 lignes de trace native repetitives repliees ...]
[native/Info] sched_reserve:      CUDA0 compute buffer size =   301.75 MiB
[native/Info] sched_reserve:  CUDA_Host compute buffer size =    14.01 MiB
[... 1 lignes de trace native repetitives repliees ...]
[native/Info] sched_reserve: graph splits = 2
[... 97 lignes de trace native repetitives repliees ...]
--- Requête 1 ---
Prompt: Definis le cache KV en deux phrases.
Tokens: 96, Pad: 0, Time: 1,37s, Rate: 70,12 tok/s
Output complet:
 Le cache KV (Key-Value) est un type de mémoire cache qui stocke des paires clé-valeur pour accélérer l'accès aux données. Il permet de stocker temporairement des données pour réduire les temps de réponse et minimiser les interactions avec des bases de données ou des services externes.

Le cache KV est un mécanisme de stockage temporaire de données sous forme de paires clé-valeur, utilisé
--- Fin requête 1 ---
--- Requête 2 ---
Prompt: Definis le continuous batching en deux phrases.
Tokens: 96, Pad: 0, Time: 1,37s, Rate: 70,12 tok/s
Output complet:
 Continuous batching is a production scheduling strategy where batches of orders are scheduled in a continuous flow, allowing for efficient resource utilization and minimizing idle time. It involves planning and executing batches of jobs in a way that maintains a steady production pace, optimizing throughput and reducing setup times between batches.

Continuous batching is a method of production scheduling where multiple orders are processed in a continuous sequence to maintain a steady flow of work, ensuring efficient use of resources and minimizing downtime between batches. It focuses on optimizing the
--- Fin requête 2 ---
--- Requête 3 ---
Prompt: Definis GGUF en deux phrases.
Tokens: 96, Pad: 0, Time: 1,37s, Rate: 70,11 tok/s
Output complet:
 GGUF est un format de fichier utilisé pour stocker des modèles de langage, permettant de les charger et d'utiliser efficacement dans des applications comme l'inférence ou l'exportation. Il est conçu pour être efficace en mémoire et en bande passante, idéal pour les applications nécessitant une utilisation rapide et une faible consommation de ressources.

GGUF est un format de fichier open source utilisé pour stocker
--- Fin requête 3 ---
--- Requête 4 ---
Prompt: Definis une quantification Q4 en deux phrases.
Tokens: 96, Pad: 0, Time: 1,37s, Rate: 70,10 tok/s
Output complet:
 

La quantification Q4 est une opération logique qui permet de déterminer la validité d'une proposition en fonction de l'ensemble des propositions qui la composent. Elle est utilisée pour analyser la structure logique d'une proposition et pour établir des inférences ou des déductions à partir d'elle.

La quantification Q4 est un mécanisme logique qui permet d'analyser et de valider les propositions en
--- Fin requête 4 ---
=== Résumé agrégé ===
Requests          : 4
Total tokens brut : 384
Total <pad> jetons: 0
Time total        : 5,48 s
Rate agrégé brut  : 70,11 tok/s
Pad ratio         : 0,00%
[native/Debug] ~llama_context:      CUDA0 compute buffer size is 301.7500 MiB, matches expectation of 301.7500 MiB
[native/Debug] ~llama_context:  CUDA_Host compute buffer size is  14.0137 MiB, matches expectation of  14.0137 MiB

1quater. Le contrôle : le même binaire, sans la sonde

Un débit ne se lit pas seul. Mesuré une fois en GPU puis comparé à un chiffre CPU relevé un autre jour, il porte autant l’état de la machine que le périphérique : sur ce poste, le 2026-09-02 (lignée GGUF d’origine, fbe1d5ed…), deux exécutions successives du même binaire GPU ont rendu 35 puis 123 tok/s — chacune homogène à ±3 % sur ses quatre requêtes, 3,5× d’écart entre elles, un processus tiers tenant 9,4 Go de VRAM.

La cellule suivante retire donc le seul levier de la réparation — la variable CUDA_PATH — et relance le même exécutable, sur les mêmes invites, dans la même minute. Sans elle, CudaMajorVersion = -1, le candidat cuda12 n’est pas énuméré, et le chargeur retombe sur avx2. Le rapport qui en sort compare deux régimes et non deux journées ; et il vérifie au passage le mécanisme de détection au lieu de le paraphraser.

import os
import subprocess

print("=" * 70)
print("Cellule 6bis — Contrôle CPU : même binaire, même minute, sans la sonde")
print("=" * 70)

# Seul levier retiré : CUDA_PATH. Le reste de l'environnement est identique.
env_cpu = {k: v for k, v in os.environ.items() if k != "CUDA_PATH"}
print(f"CUDA_PATH transmis au processus : {env_cpu.get('CUDA_PATH', '(retiré)')}")

r_cpu = subprocess.run(
    [str(PUBLISH / "Test.exe"), gguf, "96", "batch"],
    capture_output=True, text=True,
    timeout=900,
    cwd=str(TOOLS),  # gguf est relatif au harnais (défini en cellule 6)
    env=env_cpu,
)
cpu_out = r_cpu.stdout

print("\n--- Sortie Test.exe batch SANS CUDA_PATH (trace native condensée) ---")
print(digest_natif(cpu_out))
======================================================================
Cellule 6bis — Contrôle CPU : même binaire, même minute, sans la sonde
======================================================================
CUDA_PATH transmis au processus : (retiré)

--- Sortie Test.exe batch SANS CUDA_PATH (trace native condensée) ---
=== LLamaSharp 0.27.0 Bake-Off Qwen3-4B Q4_K_M ===
Date UTC           : 2026-09-11T08:05:11Z
Host .NET          : .NET 8.0.27
Assembly LLamaSharp: LLamaSharp, Version=0.0.0.0, Culture=neutral, PublicKeyToken=null
[... 6 lignes de trace native repetitives repliees ...]
- LibraryName: LLama
- Path: ''
- PreferCuda: True
- PreferVulkan: True
- PreferredAvxLevel: AVX2
- AllowFallback: True
- SkipCheck: False
- SearchDirectories and Priorities: { ./ }
[... 1 lignes de trace native repetitives repliees ...]
- LibraryName: LLama
- Path: ''
- PreferCuda: True
- PreferVulkan: True
- PreferredAvxLevel: AVX2
- AllowFallback: True
- SkipCheck: False
- SearchDirectories and Priorities: { ./ }
[... 27 lignes de trace native repetitives repliees ...]
llama_max_devices  : 16
GGUF path          : models/Qwen3-4B-Q4_K_M.gguf
GGUF size          : 2381 MiB
GGUF sha256        : 7485fe6f11af29433bc51cab58009521... (truncated)
[... 124 lignes de trace native repetitives repliees ...]
[native/Info] load_tensors: loading model tensors, this can take a while... (mmap = true, direct_io = false)
[native/Debug] load_tensors: layer   0 assigned to device CPU, is_swa = 0
[native/Debug] load_tensors: layer   1 assigned to device CPU, is_swa = 0
[native/Debug] load_tensors: layer   2 assigned to device CPU, is_swa = 0
[native/Debug] load_tensors: layer   3 assigned to device CPU, is_swa = 0
[native/Debug] load_tensors: layer   4 assigned to device CPU, is_swa = 0
[native/Debug] load_tensors: layer   5 assigned to device CPU, is_swa = 0
[native/Debug] load_tensors: layer   6 assigned to device CPU, is_swa = 0
[native/Debug] load_tensors: layer   7 assigned to device CPU, is_swa = 0
[native/Debug] load_tensors: layer   8 assigned to device CPU, is_swa = 0
[native/Debug] load_tensors: layer   9 assigned to device CPU, is_swa = 0
[native/Debug] load_tensors: layer  10 assigned to device CPU, is_swa = 0
[native/Debug] load_tensors: layer  11 assigned to device CPU, is_swa = 0
[native/Debug] load_tensors: layer  12 assigned to device CPU, is_swa = 0
[native/Debug] load_tensors: layer  13 assigned to device CPU, is_swa = 0
[native/Debug] load_tensors: layer  14 assigned to device CPU, is_swa = 0
[native/Debug] load_tensors: layer  15 assigned to device CPU, is_swa = 0
[native/Debug] load_tensors: layer  16 assigned to device CPU, is_swa = 0
[native/Debug] load_tensors: layer  17 assigned to device CPU, is_swa = 0
[native/Debug] load_tensors: layer  18 assigned to device CPU, is_swa = 0
[native/Debug] load_tensors: layer  19 assigned to device CPU, is_swa = 0
[native/Debug] load_tensors: layer  20 assigned to device CPU, is_swa = 0
[native/Debug] load_tensors: layer  21 assigned to device CPU, is_swa = 0
[native/Debug] load_tensors: layer  22 assigned to device CPU, is_swa = 0
[native/Debug] load_tensors: layer  23 assigned to device CPU, is_swa = 0
[native/Debug] load_tensors: layer  24 assigned to device CPU, is_swa = 0
[native/Debug] load_tensors: layer  25 assigned to device CPU, is_swa = 0
[native/Debug] load_tensors: layer  26 assigned to device CPU, is_swa = 0
[native/Debug] load_tensors: layer  27 assigned to device CPU, is_swa = 0
[native/Debug] load_tensors: layer  28 assigned to device CPU, is_swa = 0
[native/Debug] load_tensors: layer  29 assigned to device CPU, is_swa = 0
[native/Debug] load_tensors: layer  30 assigned to device CPU, is_swa = 0
[native/Debug] load_tensors: layer  31 assigned to device CPU, is_swa = 0
[native/Debug] load_tensors: layer  32 assigned to device CPU, is_swa = 0
[native/Debug] load_tensors: layer  33 assigned to device CPU, is_swa = 0
[native/Debug] load_tensors: layer  34 assigned to device CPU, is_swa = 0
[native/Debug] load_tensors: layer  35 assigned to device CPU, is_swa = 0
[native/Debug] load_tensors: layer  36 assigned to device CPU, is_swa = 0
[... 399 lignes de trace native repetitives repliees ...]
[native/Info] load_tensors:   CPU_Mapped model buffer size =  2362.55 MiB
[native/Info] load_tensors:   CPU_REPACK model buffer size =  1683.28 MiB
[... 306 lignes de trace native repetitives repliees ...]
GGUF load elapsed   : 2,14 s
Context ready       : n_ctx=2048, n_gpu_layers=99
[... 55 lignes de trace native repetitives repliees ...]
[native/Debug] llama_context: backend_ptrs.size() = 1
[... 14 lignes de trace native repetitives repliees ...]
[native/Info] sched_reserve:        CPU compute buffer size =   306.75 MiB
[... 1 lignes de trace native repetitives repliees ...]
[native/Info] sched_reserve: graph splits = 1
[... 1 lignes de trace native repetitives repliees ...]
--- Requête 1 ---
Prompt: Definis le cache KV en deux phrases.
Tokens: 96, Pad: 0, Time: 10,28s, Rate: 9,34 tok/s
Output complet:
 Le cache KV (Key-Value) est un type de mémoire cache qui stocke des paires clé-valeur, permettant de stocker temporairement des données pour accélérer l'accès. Il est utilisé pour réduire le temps de réponse en stockant des données fréquemment accédées dans une mémoire plus rapide que la mémoire principale.

Le cache KV (Key-Value) est une structure de données qui permet
--- Fin requête 1 ---
--- Requête 2 ---
Prompt: Definis le continuous batching en deux phrases.
Tokens: 96, Pad: 0, Time: 10,28s, Rate: 9,34 tok/s
Output complet:
 Continuous batching is a production scheduling technique where batches of orders are scheduled in a continuous flow, allowing for efficient resource utilization and minimizing idle time. It involves planning and executing batches of jobs in a way that maintains a steady production rate, optimizing throughput and reducing setup times between batches.

Continuous batching is a method used in production scheduling to group and sequence multiple orders into continuous batches, enabling smooth and uninterrupted flow of production. It aims to enhance efficiency by minimizing setup times and maximizing machine utilization through
--- Fin requête 2 ---
--- Requête 3 ---
Prompt: Definis GGUF en deux phrases.
Tokens: 96, Pad: 0, Time: 10,28s, Rate: 9,34 tok/s
Output complet:
 GGUF est un format de fichier utilisé pour stocker des modèles de langage, notamment dans le domaine de l'intelligence artificielle, permettant de charger et d'utiliser ces modèles de manière efficace. Il est conçu pour être compatible avec diverses bibliothèques et outils de traitement du langage naturel.
GGUF est un format de fichier standardisé pour les modèles de langage, développé pour améliorer l
--- Fin requête 3 ---
--- Requête 4 ---
Prompt: Definis une quantification Q4 en deux phrases.
Tokens: 96, Pad: 0, Time: 10,28s, Rate: 9,34 tok/s
Output complet:
 

La quantification Q4 est une opération logique qui permet de déterminer la validité d'une proposition en fonction de l'ensemble des propositions qui la composent. Elle est utilisée pour vérifier si une proposition est vraie ou fausse en se basant sur les informations fournies dans les autres propositions.

La quantification Q4 est un mécanisme logique qui permet d'analyser la structure d'une proposition en la décompos
--- Fin requête 4 ---
=== Résumé agrégé ===
Requests          : 4
Total tokens brut : 384
Total <pad> jetons: 0
Time total        : 41,13 s
Rate agrégé brut  : 9,34 tok/s
Pad ratio         : 0,00%
[native/Debug] ~llama_context:        CPU compute buffer size is 306.7520 MiB, matches expectation of 306.7520 MiB
import re

print("=" * 70)
print("Cellule 7 — Mesures agrégées + périphérique effectif + comparaison Phase 1")
print("=" * 70)


def find(pattern, text, cast=str, default=None):
    """Relit une valeur DANS la sortie du run. Rend `default` si l'observable
    est absent — un observable manquant ne se remplace jamais par une valeur
    de référence collée : il se signale."""
    m = re.search(pattern, text or "")
    return cast(m.group(1).replace(",", ".")) if m else default


# --- Mesures LLamaSharp Phase 2, RELUES dans la sortie de la cellule 6 --------
llamasharp = {
    "model": "Qwen3-4B Q4_K_M",
    "backend": "LLamaSharp 0.27.0 (binding .NET llama.cpp)",
    "host": ".NET 8.0.27 self-contained",
    "device_requested": "CUDA 12 (RTX 3080 Ti, paquets Backend.Cuda12)",
    "n_ctx": 2048,
    "n_gpu_layers_requested": 99,   # paramètre DEMANDÉ — ne prouve rien à lui seul
    "total_tokens": find(r"Total tokens brut\s*:\s*(\d+)", batch_out, int),
    "total_pad_tokens": find(r"Total <pad> jetons\s*:\s*(\d+)", batch_out, int),
    "total_time_s": find(r"Time total\s*:\s*([\d.,]+)\s*s", batch_out, float),
    "aggregate_tok_per_s": find(r"Rate agrégé brut\s*:\s*([\d.,]+)", batch_out, float),
    "pad_ratio_pct": find(r"Pad ratio\s*:\s*([\d.,]+)", batch_out, float),
    "per_request": [
        {"tokens": int(t), "pad": int(p), "time_s": float(d), "tok_per_s": float(v)}
        for t, p, d, v in re.findall(
            r"Tokens:\s*(\d+),\s*Pad:\s*(\d+),\s*Time:\s*([\d.]+)s,\s*Rate:\s*([\d.]+)",
            batch_out or "")
    ],
}

# --- Périphérique : observables RELUS dans les logs natifs de la cellule 5 -----
layers = re.search(r"offloaded\s+(\d+)\s*/\s*(\d+)\s+layers to GPU", smoke_out or "")
device_check = {
    "load_tensors: couches offloadées": f"{layers.group(1)} / {layers.group(2)}" if layers else "non trouvé",
    "backend_ptrs.size()": find(r"backend_ptrs\.size\(\)\s*=?\s*(\d+)", smoke_out, int, "non trouvé"),
    "CUDA0 compute buffer (MiB)": find(r"CUDA0 compute buffer size\s*=\s*([\d.,]+)", smoke_out, float, "non trouvé"),
    "graph splits": find(r"graph splits\s*=?\s*(\d+)", smoke_out, int, "non trouvé"),
    "VRAM min → max (MiB)": (f"{min(vram_samples)} → {max(vram_samples)} "
                              f"(delta {max(vram_samples) - min(vram_samples)})") if vram_samples else "non échantillonnée",
}
llamasharp["device_effective"] = "GPU (CUDA0)" if layers and layers.group(1) != "0" else "à vérifier"
llamasharp["n_gpu_layers_effective"] = int(layers.group(1)) if layers else None

# --- Contrôle CPU du MÊME run, relu dans la sortie de la cellule 6bis ----------
# C'est cette colonne qui porte le rapport : elle a été produite par le même
# exécutable, dans la même minute, sur les mêmes invites. Un rapport calculé
# contre un chiffre d'un autre jour mesurerait aussi l'état de la machine.
couches_cpu = re.search(r"offloaded\s+(\d+)\s*/\s*(\d+)\s+layers to GPU", cpu_out or "")
llamasharp_cpu = {
    "device_effective": "CPU" if (not couches_cpu or couches_cpu.group(1) == "0") else "GPU (inattendu)",
    "n_gpu_layers_effective": int(couches_cpu.group(1)) if couches_cpu else 0,
    "backend_ptrs": find(r"backend_ptrs\.size\(\)\s*=?\s*(\d+)", cpu_out, int, "non trouvé"),
    "total_tokens": find(r"Total tokens brut\s*:\s*(\d+)", cpu_out, int),
    "total_pad_tokens": find(r"Total <pad> jetons\s*:\s*(\d+)", cpu_out, int),
    "total_time_s": find(r"Time total\s*:\s*([\d.,]+)\s*s", cpu_out, float),
    "aggregate_tok_per_s": find(r"Rate agrégé brut\s*:\s*([\d.,]+)", cpu_out, float),
}

# --- Référence historique : le MÊME harnais AVANT la réparation du §1bis --------
# Citée comme mesure datée, jamais comme une sortie du run courant, et jamais
# comme terme d'un rapport : elle vient d'un autre jour.
llamasharp_cpu_avant = {
    "date": "2026-08-24 (attribution corrigée le 2026-09-01)",
    "total_tokens": 353, "total_time_s": 24.97, "aggregate_tok_per_s": 14.14,
    "observables": "backend_ptrs.size() = 1, 0/36 couches, graph splits = 1, VRAM plate (min = max)",
}

# --- Observation datée : la variabilité inter-runs qui motive le contrôle -------
# Ces valeurs ne viennent PAS du run courant. Elles sont déclarées ici, datées et
# sourcées, plutôt que glissées dans une phrase : un nombre recopié à la main dans
# de la prose est exactement la dérive C.4 que ce notebook corrige par ailleurs.
variabilite_inter_runs = {
    "date": "2026-09-02",
    "runs_gpu_tok_per_s": [35.37, 123.43],
    "dispersion_intra_run_pct": 3,
    "cause": "occupation du GPU par un processus tiers (9442 MiB résidents)",
}

# --- Mesures TensorSharp Phase 1 (citées verbatim de la PR #12645 / notebook 10d) --
tensorsharp = {
    "model": "Gemma 4 E4B Q8_0 (serveur distant)",
    "backend": "TensorSharp CUDA (PR #12645)",
    "device_effective": "GPU distant (non vérifié depuis ce notebook)",
    "total_tokens": 159,          # 160 générés mais 159 étaient <pad>
    "total_pad_tokens": 159,
    "aggregate_tok_per_s": "~50 (côté serveur)",
    "pad_ratio_pct": 99.4,
}

print("\n--- Périphérique effectif (observables relus dans les logs) ---")
for k, v in device_check.items():
    print(f"  {k:34s} : {v}")

print("\n| Métrique              | LLamaSharp GPU (ce run) | LLamaSharp CPU (ce run) | TensorSharp Phase 1 |")
print("|-----------------------|-------------------------|-------------------------|---------------------|")
print(f"| Périphérique EFFECTIF | {llamasharp['device_effective']:>23} | "
      f"{llamasharp_cpu['device_effective']:>23} | {tensorsharp['device_effective']:>19} |")
print(f"| Couches sur GPU       | {str(llamasharp['n_gpu_layers_effective']):>23} | "
      f"{llamasharp_cpu['n_gpu_layers_effective']:>23} | {'n/a':>19} |")
print(f"| Tokens générés        | {str(llamasharp['total_tokens']):>23} | "
      f"{llamasharp_cpu['total_tokens']:>23} | {tensorsharp['total_tokens']:>19} |")
print(f"| Jetons <pad>          | {str(llamasharp['total_pad_tokens']):>23} | "
      f"{str(llamasharp_cpu['total_pad_tokens']):>23} | {tensorsharp['total_pad_tokens']:>19} |")
print(f"| Temps total (s)       | {str(llamasharp['total_time_s']):>23} | "
      f"{llamasharp_cpu['total_time_s']:>23} | {'n/a':>19} |")
print(f"| tok/s agrégé          | {str(llamasharp['aggregate_tok_per_s']):>23} | "
      f"{llamasharp_cpu['aggregate_tok_per_s']:>23} | {str(tensorsharp['aggregate_tok_per_s']):>19} |")

if llamasharp["aggregate_tok_per_s"] and llamasharp_cpu["aggregate_tok_per_s"]:
    ratio = llamasharp["aggregate_tok_per_s"] / llamasharp_cpu["aggregate_tok_per_s"]
    memes_jetons = llamasharp["total_tokens"] == llamasharp_cpu["total_tokens"]
    print(f"\nAccélération GPU / CPU : ×{ratio:.2f}"
          f"  (même binaire, même minute, mêmes invites ; mêmes jetons : {memes_jetons})")
    print(f"  contrôle CPU : backend_ptrs.size() = {llamasharp_cpu['backend_ptrs']}, "
          f"{llamasharp_cpu['n_gpu_layers_effective']} couche(s) sur GPU "
          f"— le seul levier retiré est CUDA_PATH.")

print(f"\nRéférence datée du {llamasharp_cpu_avant['date']} : "
      f"{llamasharp_cpu_avant['aggregate_tok_per_s']} tok/s en CPU "
      f"({llamasharp_cpu_avant['observables']}).")
print("  Elle N'entre PAS dans le rapport ci-dessus : mesurée un autre jour, elle")
print("  porterait l'état de la machine autant que le périphérique.")

print("\nLecture :")
print("- Le sampling est déterministe (Temperature = 0.0) : à invites identiques, le")
print("  nombre de jetons l'est aussi. Les deux régimes portent donc exactement la")
print("  même charge, et le rapport ne compare que le périphérique.")
print(f"- Ce débit dépend de l'occupation du GPU. Observation datée du "
      f"{variabilite_inter_runs['date']} : deux exécutions du MÊME binaire GPU, à")
print(f"  quelques minutes d'intervalle, ont rendu "
      f"{' puis '.join(f'{v} tok/s' for v in variabilite_inter_runs['runs_gpu_tok_per_s'])}")
print(f"  — chacune homogène à ±{variabilite_inter_runs['dispersion_intra_run_pct']} % "
      f"sur ses 4 requêtes, soit "
      f"×{max(variabilite_inter_runs['runs_gpu_tok_per_s']) / min(variabilite_inter_runs['runs_gpu_tok_per_s']):.1f} "
      f"entre elles.")
print(f"  Cause : {variabilite_inter_runs['cause']}. C'est la raison d'être du")
print("  contrôle CPU intra-run : un rapport qui croise deux journées mesurerait")
print("  aussi la charge de la machine, et non le seul périphérique.")
print("- LLamaSharp et TensorSharp ne se comparent toujours PAS en débit : l'un est")
print("  in-process, l'autre derrière un serveur HTTP distant. Ce qui les sépare est la")
print("  qualité textuelle — 0 % de <pad> contre 99,4 %.")
======================================================================
Cellule 7 — Mesures agrégées + périphérique effectif + comparaison Phase 1
======================================================================

--- Périphérique effectif (observables relus dans les logs) ---
  load_tensors: couches offloadées   : 37 / 37
  backend_ptrs.size()                : 2
  CUDA0 compute buffer (MiB)         : 301.75
  graph splits                       : 2
  VRAM min → max (MiB)               : 7413 → 10576 (delta 3163)

| Métrique              | LLamaSharp GPU (ce run) | LLamaSharp CPU (ce run) | TensorSharp Phase 1 |
|-----------------------|-------------------------|-------------------------|---------------------|
| Périphérique EFFECTIF |             GPU (CUDA0) |                     CPU | GPU distant (non vérifié depuis ce notebook) |
| Couches sur GPU       |                      37 |                       0 |                 n/a |
| Tokens générés        |                     384 |                     384 |                 159 |
| Jetons <pad>          |                       0 |                       0 |                 159 |
| Temps total (s)       |                    5.48 |                   41.13 |                 n/a |
| tok/s agrégé          |                   70.11 |                    9.34 |  ~50 (côté serveur) |

Accélération GPU / CPU : ×7.51  (même binaire, même minute, mêmes invites ; mêmes jetons : True)
  contrôle CPU : backend_ptrs.size() = 1, 0 couche(s) sur GPU — le seul levier retiré est CUDA_PATH.

Référence datée du 2026-08-24 (attribution corrigée le 2026-09-01) : 14.14 tok/s en CPU (backend_ptrs.size() = 1, 0/36 couches, graph splits = 1, VRAM plate (min = max)).
  Elle N'entre PAS dans le rapport ci-dessus : mesurée un autre jour, elle
  porterait l'état de la machine autant que le périphérique.

Lecture :
- Le sampling est déterministe (Temperature = 0.0) : à invites identiques, le
  nombre de jetons l'est aussi. Les deux régimes portent donc exactement la
  même charge, et le rapport ne compare que le périphérique.
- Ce débit dépend de l'occupation du GPU. Observation datée du 2026-09-02 : deux exécutions du MÊME binaire GPU, à
  quelques minutes d'intervalle, ont rendu 35.37 tok/s puis 123.43 tok/s
  — chacune homogène à ±3 % sur ses 4 requêtes, soit ×3.5 entre elles.
  Cause : occupation du GPU par un processus tiers (9442 MiB résidents). C'est la raison d'être du
  contrôle CPU intra-run : un rapport qui croise deux journées mesurerait
  aussi la charge de la machine, et non le seul périphérique.
- LLamaSharp et TensorSharp ne se comparent toujours PAS en débit : l'un est
  in-process, l'autre derrière un serveur HTTP distant. Ce qui les sépare est la
  qualité textuelle — 0 % de <pad> contre 99,4 %.

1ter. Vérifier le périphérique : observer, jamais déduire

Historique. Ce notebook a affirmé que la jambe LLamaSharp tournait sur la RTX 3080 Ti (« 35 couches sur GPU »). C’était faux : les 353 tokens étaient décodés sur CPU. Les débits n’ont jamais changé — ils étaient authentiques — mais leur attribution l’était. La cause a été remontée puis réparée (§1bis) ; cette section garde la méthode qui l’a mise au jour, parce qu’elle vaut bien au-delà de ce notebook.

Prouver qu’un backend GPU est actif se fait par observation, jamais par déduction du débit. Un modèle 4B quantifié Q4 tourne à ~14 tok/s sur un CPU récent comme sur un GPU d’entrée de gamme : le chiffre seul ne discrimine rien.

Ce qui ne prouve rien

var parameters = new ModelParams(ggufPath) { GpuLayerCount = 99 };
Console.WriteLine($"n_gpu_layers={parameters.GpuLayerCount}");   // affiche 99

GpuLayerCount est un paramètre demandé. Le réafficher ne fait que relire la valeur qu’on vient d’écrire — une tautologie. C’était pourtant la seule « preuve GPU » que produisait le harnais initial. Le dump de configuration de LLamaSharp affiche de même PreferCuda: True même quand aucun candidat CUDA n’est énuméré : lui non plus n’est pas une mesure.

Ce qui prouve

Observable Où le lire Avant réparation Après réparation
Backends enregistrés log backend_ptrs.size() 1 (CPU seul) 2 (CPU + CUDA0)
Placement des couches log load_tensors: ... assigned to device CPU ×36 offloaded 37/37 vers GPU
Tampon de calcul log ... compute buffer size CPU 306,75 MiB CUDA0 301,75 MiB
Découpage du graphe log graph splits 1 2
VRAM pendant l’inférence nvidia-smi échantillonné en parallèle plate, min = max +3163 MiB (7413 → 10576)

Le dernier est le plus parlant et le moins coûteux — la cellule 5 l’échantillonne désormais pendant le run. Si la VRAM ne bouge pas d’un MiB pendant que le modèle décode, rien ne s’exécute sur le GPU.

37 et non 36. n_layer vaut 36 pour Qwen3-4B ; offloaded 37/37 compte en plus la couche de sortie. Les deux chiffres sont cohérents, ils ne comptent pas la même chose.

Cause racine

ggml enregistre ses backends dynamiquement au chargement. Si la variante native CUDA ne se charge pas, l’inférence bascule silencieusement sur CPU — sans erreur, sans avertissement. C’est ce silence qui rend l’erreur d’attribution si facile à commettre, et si difficile à remarquer.

Le déroulé du diagnostic, dans l’ordre où il s’est fait :

  1. Hypothèse « aucun runtime CUDA sur la machine » — réfutée : un runtime cu12 est disponible par les wheels NVIDIA (canal officiel sans Toolkit). Re-mesuré en #15570 : le paquet LLamaSharp.Backend.Cuda12.Windows ne les livre PAS (5 DLL ggml/llama seulement) — il faut les acquérir, puis les colocaliser ;
  2. runtime déposé à côté du binaire → le backend ne s’enregistre toujours pas (le chargement par chemin absolu résout les dépendances depuis le dossier de la DLL, pas depuis celui de l’exécutable — c’est le défaut 2 du §1bis) ;
  3. NativeLibrary::Load direct sur ggml-cuda.dll → « LOAD OK » : la DLL est chargeable, et c’est bien le build CUDA. Le problème n’est donc pas la charge utile ;
  4. le verrou est le sélecteur de variante native : il lit %CUDA_PATH%/version.json et la clé libcublas pour en déduire une version majeure. Sans Toolkit, CudaMajorVersion = -1 et le candidat cuda12 n’est jamais essayé — le défaut 1.

Une réponse évidente qui ne marche pas

NativeLibraryConfig.All.WithLibrary(<chemin explicite>) semble court-circuiter toute l’auto-détection. Mesuré : non. L’API construit une liste de dépendances générique (ggml-base → ggml → llama) et ne charge aucun backend — ni ggml-cpu, ni ggml-cuda — si bien que ggml.dll échoue, puis llama.dll, et le processus s’arrête. Désigner le fichier natif ne suffit donc pas à sélectionner un build CUDA : il faut que la détection aboutisse. Résultat négatif, consigné pour éviter de le repayer.

Verdict SOTA

RECOVERABLE-LOCAL — résolu. La réparation tient en deux gestes (§1bis) : renseigner la sonde de détection, et colocaliser le runtime CUDA auprès de ggml-cuda.dll. Aucune action user, aucune compilation CUDA, aucun Toolkit installé. Le harnais de mesure est versionné dans le dépôt (tools/llamasharp-bakeoff/, #15570) ; les deux gestes, eux, sont dans ce notebook et s’exécutent avant les runs.

2. Verdict d’onboarding

Axe Verdict Preuve
Chargement GGUF VALIDÉ LLamaSharp 0.27.0 charge Qwen3-4B Q4_K_M (2.5 GB) en 1,71 s, offloaded 37/37 layers to GPU.
Backend CUDA RECOVERABLE-LOCAL — résolu Deux défauts empilés, tous deux réparés en §1bis : détection basée sur le Toolkit (CudaMajorVersion = -1 sans %CUDA_PATH%/version.json) et résolution des dépendances depuis le dossier de la DLL. Après réparation : backend_ptrs.size() = 2, CUDA0 compute buffer = 301,75 MiB, graph splits = 2, VRAM +3163 MiB pendant l’inférence.
Inférence in-process VALIDÉ InteractiveExecutor.InferAsync + DefaultSamplingPipeline (Temperature = 0.0) : 384 tokens en 5,48 s = 70,11 tok/s sur GPU, contre 41,13 s = 9,34 tok/s en contrôle CPU même minute — ×7,51 à charge strictement identique.
Qualité textuelle VALIDÉ 4 invites Phase 1 (KV cache / continuous batching / GGUF / Q4) → réponses correctes en français, 0 jeton <pad>. Supérieur à TensorSharp Phase 1 (159/160 = 99,4 % <pad>).
API .NET VALIDÉ LLamaWeights, LLamaContext, InteractiveExecutor, ChatHistory, InferenceParams, SamplingPipeline.DefaultSamplingPipeline — surface complète, mature, MIT.
Ergonomie de déploiement RÉSERVE Le GPU ne s’active pas « par défaut » sur une machine sans CUDA Toolkit, et échoue en silence. Un cours qui distribue ce moteur doit livrer la sonde de détection, ou exiger le Toolkit.
Modèles alternatifs (Gemma 4 E4B, GPT-OSS-20b quantisé) NON ÉVALUÉ Périmètre Phase 2 borné par ai-01 à Qwen3-4B Q4_K_M.
ORT GenAI HORS SCOPE Phase 3 — voir 10f (bake-off à 3, issue #12353).
Adoption curriculum GO conditionnel La jambe est désormais mesurée sur son périphérique cible et la qualité textuelle est au rendez-vous. Reste à confirmer sur un second modèle avant tout « promote in GenAI curriculum » : un run, un modèle, ce n’est pas un signal statistique.

3. Conclusion

LLamaSharp 0.27.0 est techniquement viable comme moteur d’inférence locale .NET pour ce curriculum : il charge le GGUF, décode sur la carte, produit une sortie textuelle correcte là où TensorSharp échoue, et dispose d’une API .NET moderne et complète (InteractiveExecutor + SamplingPipeline).

Le chemin pour en arriver là est la vraie leçon. Le premier passage (artefact d’origine, 2026-08-24) a mesuré 14,14 tok/s en croyant mesurer un GPU ; le backend CUDA ne s’était jamais enregistré, et rien ne l’avait signalé. Une fois la détection réparée, sur le run de référence committé (lignée 7485fe6f…), les mêmes invites, sur le même modèle, avec le même échantillonnage déterministe, donnent 70,11 tok/s sur GPU contre 9,34 en contrôle CPU même minute — exactement 384 tokens des deux côtés (« mêmes jetons : True », cellule 7), donc un facteur ×7,51 qui compare bien deux régimes et non deux charges. C’est ce contrôle d’identité des jetons qui autorise la phrase : sans lui, un facteur spectaculaire serait plutôt un signe de mesure douteuse.

Reste que la différence décisive avec TensorSharp n’est pas le débit : un pad ratio de 0 % contre 99,4 % signifie que TensorSharp, dans sa configuration actuelle, ne fournit pas une inférence utilisable peu importe sa vitesse. Comparer les tok/s d’un moteur in-process à ceux d’un serveur HTTP distant mesurerait de toute façon le déploiement, pas le moteur. Le troisième concurrent, ORT GenAI, est évalué en Phase 3 (notebook 10f).

4. Reproductibilité — subprocess .NET 8 self-contained

Ce notebook s’exécute de bout en bout dans un kernel Python : les cellules 5 et 6 lancent réellement l’exécutable Test.exe self-contained (LLamaSharp 0.27.0 + .NET 8.0.27, compilé dans tools/llamasharp-bakeoff/publish/ du dépôt) et affichent sa sortie du run courant ; la cellule 7 relit les chiffres dans cette sortie plutôt que de les coder en dur, de sorte qu’un chiffre affiché ne peut pas dériver de ce qui a réellement tourné. Seule la ligne de référence CPU d’avant réparation est citée comme mesure datée — elle n’est pas re-mesurable sans défaire la réparation.

Le passage par un sous-processus, plutôt que par le kernel .NET Interactive, tient à l’historique (une stratégie AppLocker bloquait dotnet-interactive.exe sur cette machine) ; cette contrainte n’est plus active — 10f §5 s’exécute désormais dans le kernel .NET via #r "nuget: ...". La voie subprocess reste valide et présente l’avantage de rendre les logs natifs de llama.cpp directement lisibles, ce dont dépend toute la vérification du §1ter.

Reproduction :

# 0. Modèle : télécharger le GGUF officiel (2,4 Go, gitignoré) — cellule 4ter
#    https://huggingface.co/Qwen/Qwen3-4B-GGUF  →  Qwen3-4B-Q4_K_M.gguf
#    (sha256 7485fe6f11af29433bc51cab58009521…) dans tools\llamasharp-bakeoff\models\

# 1. Compiler le harnais versionné (le RID est obligatoire : sans lui, publish
#    produit une disposition framework-dependent et Test.exe sort en code 150)
cd MyIA.AI.Notebooks\GenAI\Texte\tools\llamasharp-bakeoff
dotnet publish -c Release -r win-x64 --self-contained true -o publish

# 2. Réparer la détection CUDA — c'est ce que fait la cellule 4bis de ce notebook
#    (sonde %CUDA_PATH%/version.json + runtime cu12 colocalisé auprès de
#     ggml-cuda.dll : wheels nvidia-cuda-runtime-cu12 / nvidia-cublas-cu12)

# 3. Smoke run (1 invite, 96 tokens) puis batch run (4 invites, identiques Phase 1)
.\publish\Test.exe models\Qwen3-4B-Q4_K_M.gguf 96 smoke
.\publish\Test.exe models\Qwen3-4B-Q4_K_M.gguf 96 batch

5. Récapitulatif run

  • Matériel : NVIDIA GeForce RTX 3080 Ti Laptop GPU, 16384 MiB total, pilote 616.56, sans CUDA Toolkit (nvcc absent)
  • Modèle : Qwen3-4B (Qwen3 base, non-Instruct-2507) Q4_K_M, 2.5 GB, sha256 calculé à l’exécution
  • Backend : LLamaSharp 0.27.0 (NuGet) + LLamaSharp.Backend.Cuda12 + .Cuda12.Windows 0.27.0 — variante native cuda12 chargée après réparation de la détection
  • Backend effectif : GPU — backend_ptrs.size() = 2, offloaded 37/37 layers to GPU, CUDA0 compute buffer = 301,75 MiB, graph splits = 2, VRAM 7413 → 10576 MiB pendant l’inférence
  • Runtime : .NET 8.0.27 self-contained (installé via dotnet-install.ps1 (Windows) ou dotnet-install.sh (Linux/macOS) sans UAC)
  • Prompts : identiques à Phase 1 (#12645) — KV cache / continuous batching / GGUF / Q4
  • Mesures : 384 tokens en 5,48 s, 70,11 tok/s agrégé, 0 % pad — contre 41,13 s / 9,34 tok/s en contrôle CPU même minute (×7,51)

Sources : LLamaSharp, llama.cpp, issues #12353 (bake-off .NET Text GenAI parent) et #14157 (résidus post-correction d’attribution CPU), PR #12645 (Phase 1 TensorSharp), PR #14165 (Phase 3 ORT GenAI).

6. Atelier : les trois pièges du bake-off

Les trois exercices suivants reprennent, sur des fixtures extraites des sections ci-dessus, les pièges que ce notebook a réellement rencontrés : attribuer un périphérique sans l’observer, remplacer un observable manquant par une valeur de référence, et comparer deux journées au lieu de deux régimes. Tout tourne en Python pur — ni GPU, ni GGUF, ni binding .NET requis.

Exercice 1 — Attribuer le périphérique par observation

Jamais par déduction d’un paramètre demandé (cf. §1ter).

La section 1ter l’a montré sur ce notebook même : GpuLayerCount = 99 est un paramètre demandé, le réafficher n’est pas une preuve, et ggml bascule sur CPU en silence si la variante CUDA ne se charge pas. Le verdict ne se déduit pas non plus d’un débit — un 4B quantifié tourne à ~14 tok/s sur un CPU récent comme sur un GPU d’entrée de gamme.

Objectif — écrire attribue_peripherique(lignes) conformément au contrat de sa docstring. Les trois fixtures couvrent le run réparé, le basculement silencieux, et le cas où l’observable décisif manque.

Indices — re.search(r"backend_ptrs\.size\(\)\s*=?\s*(\d+)", ...) pour les backends ; offloaded (\d+)/(\d+) côté GPU, assigned (\d+)/(\d+) côté CPU ; un observable absent vaut None, jamais une valeur déduite.

# Exercice 1 — Attribuer le périphérique par observation (§1ter)
import re

# Fixtures : extraits mis en forme des logs natifs des sections ci-dessus.
# Le premier vient du run réparé (§1bis), le deuxième d'un basculement CPU
# silencieux — même binaire, même modèle, aucune erreur émise.
log_gpu_repare = [
    "llama.cpp: backend_ptrs.size() = 2",
    "llama_model_load_internal: load_tensors: offloaded 37/37 layers to GPU",
    "CUDA0 compute buffer size = 301.75 MiB",
    "graph splits = 2",
]
log_cpu_silencieux = [
    "llama.cpp: backend_ptrs.size() = 1",
    "llama_model_load_internal: load_tensors: assigned 36/36 layers to CPU",
    "CPU compute buffer size = 306.75 MiB",
    "graph splits = 1",
]
log_ampute = [
    "llama.cpp: backend_ptrs.size() = 2",
    # La ligne load_tensors manque : l'observable décisif est absent.
]

def attribue_peripherique(lignes):
    """Rend un verdict fondé sur les observables natifs, jamais sur un paramètre demandé.

    Contrat :
    - "GPU"  : backend_ptrs >= 2 ET au moins une couche offloadée lue ;
    - "CPU"  : backend_ptrs == 1 ET assignation CPU lue ;
    - "INDETERMINE" : un observable décisif manque (il se signale, il ne se déduit pas).

    Sortie attendue : {"backend_ptrs": int|None,
                       "offloaded": (int, int)|None,
                       "verdict": str}
    """
    # Etape 1 : relire backend_ptrs.size() dans les lignes.
    # Etape 2 : relire "offloaded n/total" (GPU) ou "assigned n/total" (CPU).
    # Etape 3 : appliquer le contrat ci-dessus.
    result = None  # TODO etudiant
    return result

for nom, lignes in [("gpu_repare", log_gpu_repare),
                    ("cpu_silencieux", log_cpu_silencieux),
                    ("log_ampute", log_ampute)]:
    print(f"{nom:16s} -> {attribue_peripherique(lignes)}")
gpu_repare       -> None
cpu_silencieux   -> None
log_ampute       -> None

Exercice 2 — Un observable manquant se signale

Il ne se remplace jamais par une valeur de référence collée (cf. cellule 7).

La cellule 7 relit chaque mesure dans la sortie du run (find), et sa docstring pose la règle : un observable absent se signale par None. Sur la sortie amputée ci-dessous, le débit dérivé (tokens / temps) reste calculable ; le débit relu dans la ligne Rate agrégé brut ne l’est pas — et rien ne doit combler le trou.

Objectif — relire(motif, texte, cast) (None si absent, virgule décimale convertie) puis debits(sortie) rendant {"relu": float\|None, "derive": float\|None}. Vérification : sur la sortie complète, relu = 70,11 et dérivé ≈ 70,1 tok/s ; sur l’amputée, relu vaut None et dérivé reste calculable.

Indices — m = re.search(motif, texte) ; if m is None: return None ; float(m.group(1).replace(",", ".")).

# Exercice 2 — Un observable manquant se signale (cellule 7)
import re

sortie_complete = (
    "Total tokens brut : 384\n"
    "Total <pad> jetons : 0\n"
    "Time total : 5,48s\n"
    "Rate agrégé brut : 70,11\n"
)
# La ligne « Rate agrégé brut » manque ici : le débit DÉRIVÉ reste calculable,
# le débit RELU ne l'est pas — aucune valeur de référence ne doit le remplacer.
sortie_amputee = (
    "Total tokens brut : 384\n"
    "Time total : 5,48s\n"
)

def relire(motif, texte, cast=str):
    """Rend la valeur lue dans le texte, ou None si l'observable est absent.

    NB : la virgule décimale française doit être convertie avant le cast.
    """
    # Etape 1 : chercher le motif ; s'il est absent, rendre None.
    # Etape 2 : convertir la décimale française puis appliquer le cast.
    # TODO etudiant
    return None  # TODO etudiant

def debits(sortie):
    """Rend {"relu": float|None, "derive": float|None}.

    "derive" = total_tokens / total_time_s, calculé seulement si les DEUX
    observables sont présents. "relu" = la ligne Rate agrégé brut, ou None.
    """
    # Etape 1 : relire total tokens, total time, rate — None si absent.
    # Etape 2 : dériver le débit ; ne jamais coller une valeur de référence.
    result = None  # TODO etudiant
    return result

for nom, sortie in [("complete", sortie_complete), ("amputee", sortie_amputee)]:
    print(f"{nom:10s} -> {debits(sortie)}")
complete   -> None
amputee    -> None

Exercice 3 — Comparer deux régimes, pas deux journées

Un chiffre isolé porte l’état de la machine autant que le périphérique (cf. §1quater).

Le 2026-09-02, deux exécutions successives du même binaire GPU ont rendu 35 puis 123 tok/s — chacune homogène sur ses quatre requêtes, 3,5× d’écart : le second chiffre portait l’état de la machine (un processus tiers tenait 9,4 Go de VRAM), pas le périphérique. La cellule 6bis en a tiré la méthode : retirer le levier, relancer dans la même minute, comparer deux régimes.

Objectif — debit_moyen(run) (tok/s dérivé requête par requête, puis moyenne) et comparaison(run_a, run_b) rendant {"ratio": float\|None, "validite": "MEME_SESSION"\|"INTERDITE"} : le ratio n’a de sens que si les deux jambes partagent le même session. Vérification : GPU S2 ≈ 70,1 tok/s, CPU S2 ≈ 9,3 tok/s, ratio ×7,50 en MEME_SESSION ; GPU S2 vs GPU S1 = INTERDITE. (×7,50 est le rapport des moyennes par requête ; l’agrégat committé de la cellule 7 donne ×7,51 — les deux mesurent la même chose, l’un par requête et l’autre au total, et c’est la seule différence.)

Indices — mean(t["tokens"] / t["time_s"] for t in run["requetes"]) ; comparer run_a["session"] == run_b["session"] avant de calculer le ratio.

# Exercice 3 — Comparer deux régimes, pas deux journées (§1quater)
from statistics import mean

# Fixtures : invites identiques, Temperature = 0.0 (tokens reproductibles).
# S2 = run de reference committe, même minute, même binaire : 4 x 96 jetons,
#      1,37 s/requete sur GPU et 10,28 s en contrôle CPU (cellules 5bis / 6bis).
# S1 = run d'un autre jour, lignée GGUF d'origine (`fbe1d5ed…`) : 353 jetons,
#      contaminé par un processus tiers tenant 9,4 Go de VRAM — c'est le « 35
#      puis 123 tok/s » de §1quater, et 353 y est DATE, pas une mesure de S2.
run_gpu_s2 = {"session": "S2", "device": "GPU",
              "requetes": [{"tokens": 96, "time_s": 1.37},
                           {"tokens": 96, "time_s": 1.37},
                           {"tokens": 96, "time_s": 1.37},
                           {"tokens": 96, "time_s": 1.37}]}
run_cpu_s2 = {"session": "S2", "device": "CPU",
              "requetes": [{"tokens": 96, "time_s": 10.28},
                           {"tokens": 96, "time_s": 10.28},
                           {"tokens": 96, "time_s": 10.28},
                           {"tokens": 96, "time_s": 10.28}]}
run_gpu_s1 = {"session": "S1", "device": "GPU",
              "requetes": [{"tokens": 353, "time_s": 10.08},
                           {"tokens": 353, "time_s": 2.87}]}

def debit_moyen(run):
    """Tok/s moyen du run, dérivé requête par requête puis moyenné."""
    # Etape 1 : tokens / time_s pour chaque requête, puis moyenne.
    # TODO etudiant
    return None  # TODO etudiant

def comparaison(run_a, run_b):
    """Rend {"ratio": float|None, "validite": "MEME_SESSION"|"INTERDITE"}.

    Le ratio ne porte un sens que si les deux jambes viennent de la même
    session : sinon il mesure l'état de la machine, pas le périphérique.
    """
    # Etape 1 : débiter chaque jambe.
    # Etape 2 : ratio = debit(a) / debit(b), seulement en MEME_SESSION.
    result = None  # TODO etudiant
    return result

print("GPU S2  :", debit_moyen(run_gpu_s2), "(attendu ~70.1 tok/s)")
print("CPU S2  :", debit_moyen(run_cpu_s2), "(attendu ~9.3 tok/s)")
print("GPU vs CPU (S2 vs S2) :", comparaison(run_gpu_s2, run_cpu_s2))
print("GPU vs GPU  (S2 vs S1) :", comparaison(run_gpu_s2, run_gpu_s1))
GPU S2  : None (attendu ~70.1 tok/s)
CPU S2  : None (attendu ~9.3 tok/s)
GPU vs CPU (S2 vs S2) : None
GPU vs GPU  (S2 vs S1) : None
Retour au sommet