Le port GPU Apple Silicon — Metal direct, MLX en oracle CPU

Le port GPU Apple Silicon : Metal direct, MLX en oracle CPU

Pourquoi un second port — et pourquoi par mesure, pas par argument

Le code Monte-Carlo d'origine cite, dans la lignée méthodologique de son auteur, un principe de validation : le seul pattern fiable est une seconde implémentation, écrite par une autre équipe, avec une méthode numérique différente, sur le même problème physique. Un test unitaire avec une tolérance écrite à la main n'est qu'une reformulation de la croyance du développeur. Une implémentation indépendante est un falsificateur.

Le portage Radience honore cette discipline avec deux monomères Apple-Silicon, mais à un niveau structurel plus profond que dans la version précédente de ce chapitre : la bifurcation d'architecturequel moteur GPU pour ce workload — a été tranchée non pas par argument du panel, mais par une mesure. La première exécution réelle de MLX sur le GPU Apple — 50 lignes portant compton_mean_free_path_iron sur mlx_default_gpu_stream_new — a livré le verdict empirique :

MLX-array-ops est bandwidth-bound sur ce workload — l'inverse exact de Conway (cf. automata ADR-0005), et le contre-grain d'un calcul Monte-Carlo branchy où chaque rayon est indépendant et lit ~zéro nouvel octet par étape. La conclusion n'est pas un jugement de l'outil MLX ; c'est un jugement de l'alignement entre l'outil et le grain du calcul.

Monomère A — Metal direct, le port de production

Le Monomère A est un port Metal direct : compute shader MSL hand-écrit, un thread GPU par rayon, intersection Möller-Trumbore brute-force sur les triangles avec hit le plus proche en registres, cascade gamma scalaire par thread (comme dans le pinhole_camera.cu original). Le patron est emprunté verbatim à automata/crates/automata-metal : metal = "0.33", Device::system_default, new_library_with_source, pipeline de compute, buffers StorageModeShared zéro-copie (mémoire unifiée Apple), command-buffer → encoder → dispatch_threads → commit → wait_until_completed, lecture par contents(). Le MetalRunner implémente le port hexagonal MonteCarloRunnerPort, sibling de MlxRunner et OptiX9Runner.

AxePort OptiX 9 (cible NVIDIA)Monomère A — Metal direct (cible Apple)Monomère B — MLX array-ops (rétrogradé)
MatérielNVIDIA L4 (Ada, sm_89)Apple M-series (Metal)Apple M-series (CPU)
Modèle d'exécutionun thread par photon (OptiX)un thread par rayon (MSL compute)tenseurs batchés (matérialisés)
Régime rooflinecompute-boundcompute-bound (AI 125–500 vs ridge 29,7)bandwidth-bound (AI eff. ~0,12)
Rôleport de production NVIDIAport de production Apple Siliconoracle numérique CPU

Sur Apple M4 Max, au régime saturé (np \(\geq\) 2,4M), Monomère A tient 848 ns par photon primaire soit 1,18 million de photons/seconde — un speedup de \(33,58\times\) sur la baseline CPU mono-thread scellée, confirmant la prédiction carnot de \(30–35\times\) (zone occupancy-limitée, levier d'optimisation futur : la pile de 10 slots hors registres). La radiographie IQI Sinus à 300M photons est rendue en 5 min 50 s sur le GPU du Mac.

Anti-fabrication : faire du mensonge GPU un type illégal

Une lumière verte qui ment coûte plus cher qu'une rouge. Le Monomère A ne se contente pas d'affirmer qu'il tourne sur GPU — il prouve chaque exécution par un témoin signé par le driver, typé non-falsifiable :

pub struct BackendIdentity {
    pub api: BackendApi,
    pub device: String,
    pub executed_on: ExecutionWitness,   // non-Default, non-Deserialize
}

pub enum ExecutionWitness {
    Gpu { source: GpuWitnessSource, timestamps: GpuTimestamps /* sealed */ },
    Cpu,
}

ExecutionWitness::Gpu n'est mintable qu'à partir d'un compteur driver scellé (MTLCommandBuffer.GPUStartTime / GPUEndTime). Le sysctl machdep.cpu.brand_string qui produisait l'ancien descripteur menteur « MLX / Apple M4 Max / metal » est retiré. Le contrôle négatif RADIENCE_FORCE_CPU=1 erre par contrat — un kernel GPU qui se replierait silencieusement sur le CPU est détecté avant publication.

Le gate G_gpu_actually_ran exige \(\geq\) 2 témoins forts sur 3 (driver-counter / powermetrics / MTL-capture) plus le contrôle négatif. Sans ces preuves, aucun nombre n'est publié — l'absence de témoin est un échec, pas un résultat.

Monomère B — MLX array-ops, rétrogradé en oracle CPU numérique

La mesure ayant tranché en B contre l'usage GPU, le Monomère B est conservé mais rétrogradé :

Les 4 anomalies — préservées dans les deux monomères

Le code original porte quatre choix d'implémentation que l'analyse a identifiés comme intentionnels et à préserver (cf. § 04 + la persona auteur reconstituée) :

  1. Branche basse-énergie désactivée par tautologie (||3==3) — désactivée par design, pas par bug ; constante-foldée en MSL \(\to\) coût runtime nul, fidélité préservée.
  2. Profondeur de cascade fixée à 10 — calibrée empiriquement sur les runs IQI.
  3. Valeurs Z tungstène/fer codées en dur — les deux seuls matériaux de la campagne ; tables compile-time en MSL, jamais une branche runtime.
  4. Générateur RNG xorshift32 — choix idiomatique de l'époque (GPU Kepler/Maxwell 2017) ; bit-exact entre les monomères, RNG bit-stable est l'oracle qu'on certifie.

Ces anomalies sont préservées verbatim dans Monomère A (MSL) et Monomère B (Rust CPU). Un gate (G_anomalies_preserved_runtime) vérifie leur présence au niveau runtime/comportemental — pas seulement textuel, parce qu'un compilateur agressif pourrait réordonner ou éliminer le ||3==3 sous -ffast-math (interdit explicitement sur les TU de physique). Préserver une anomalie n'est pas la corriger : c'est honorer le fait que le code porté doit d'abord reproduire la baseline avant qu'on en discute les améliorations.