Skip to content

7 draft

Voici les fondations, sans rédaction :


1. Le problème actuel

Points à couvrir, dans l'esprit de la table --jit-profile de blog/5 :

  • Le tableau Iters/Sessions/RSS tel quel (30/500/5000 itérations) — c'est ton meilleur "hook" d'ouverture, comme le tableau de benchmark NanoGPT en intro de blog/5.
  • La distinction device vs host : IREE device allocator: peak 4KB, live 0KB vs phys_footprint (dirty): 7.85GB — ce contraste est frappant et mérite d'être mis en avant tôt, ça évite au lecteur de penser "fuite GPU classique".
  • Le mécanisme de ping-pong en 4 étapes (shape A compile → shape B invalide A, compile → shape A invalide B, recompile...) — probablement le meilleur candidat pour être présenté comme un eprintln trace réel, dans le style "code block observé" que tu utilises pour le IREE dispatch profile de blog/5.
  • Pourquoi ce n'est pas une bizarrerie CLEVR : le polymorphisme de shape est le cas normal en Sheaf, pas l'exception (à relier à ta philosophie "JAX-flavored Lisp" de blog/1).

2. Diagramme ASCII #1 — fonctionnement actuel

FunctionDef "sigmoid"
  └── vmfb_session_idx: Option<usize>   ← un seul slot, jamais un ensemble

Appel 1: sigmoid(f32[1,5])
  vmfb_session_idx = None → compile → push #0 dans vmfb_sessions
  vmfb_session_idx = Some(0)

Appel 2: sigmoid(f32[1,5,1])
  check_shapes_match([1,5] vs [1,5,1]) → MISMATCH
  vmfb_session_idx cleared → compile → push #1
  vmfb_session_idx = Some(1)        ← session #0 toujours dans le Vec, mais orpheline

Appel 3: sigmoid(f32[1,5])
  check_shapes_match([1,5,1] vs [1,5]) → MISMATCH
  vmfb_session_idx cleared → compile → push #2
  vmfb_session_idx = Some(2)        ← #0 et #1 orphelines

vmfb_sessions: [ #0(dead) #1(dead) #2(live) ... ]  ← append-only, jamais purgé
                └────────────┬────────────┘
                   ~106MB × sessions mortes

Idée pour la légende : montrer que le graphe de vie "un seul index vivant" contredit visuellement la croissance du Vec en dessous — c'est le cœur du bug en une image.

3. Comment JAX fonctionne

  • Structure : xla::LRUCache<CallSignature, ...> par PjitFunction, capacité par défaut 4096.
  • Ce qui compose CallSignature (jax_jit.h:141) : shape/dtype des args, structure pytree, sharding, device, config — donc deux shapes ≠ deux entrées coexistantes, pas une invalidation.
  • Le point clé pour ton lecteur : JAX traite le changement de shape comme un cache miss ordinaire, jamais comme une erreur qui invalide l'existant.
  • Mentionner le disk cache secondaire (recompilation évitée entre process).
  • static_argnums comme seul mécanisme voulu de thrashing, documenté comme un risque assumé par l'utilisateur — bon contrepoint pour montrer que même JAX peut thrasher, mais volontairement.

4. Comment PyTorch fonctionne

  • Cache Dynamo : liste chaînée de (guard_manager, code, next) sur co_extra, walk + premier match gagne.
  • Les deux limites (recompile_limit=8 par région, accumulated_recompile_limit=256 global) et surtout la dégradation explicite vers eager quand la limite est dépassée — c'est le contraste le plus utile avec Sheaf actuel, qui lui n'a aucun mécanisme de dégradation (il crashait avant le fix de clear_failure, maintenant il boucle indéfiniment).
  • Automatic dynamic shapes : le mécanisme de "wobble" sur une dimension → une seule recompile avec dimension symbolique, N valeurs futures collapsent sur une entrée. Bon endroit pour introduire Option E et pourquoi elle reste hors scope (dépend de shapes dynamiques dans la pipeline de lowering, absentes de Sheaf aujourd'hui).
  • Point structurel important pour la clarté de l'article : bien montrer que la stratégie multi-entrée (A) est le socle commun à JAX et PyTorch, et que les dynamic shapes sont un raffinement PyTorch par-dessus ce socle, pas une alternative.

5. Le choix retenu, et pourquoi on écarte les autres

  • Retenu : D puis A. D (runtime/session IREE partagé, modules chargés par nom) règle le coût par artefact (~106MB observés) ; A (cache multi-shape borné par fonction, LRU) règle le nombre d'artefacts retenus. Le séquençage D-avant-A isole le refactor FFI risqué (lifetimes, Drop) et le rend testable indépendamment (RSS par module chargé avant/après).
  • B (interpreter-only après seuil de thrash) écarté comme fix principal : contredit l'idiome central de Sheaf — le polymorphisme de shape est censé être gratuit, pas pénalisé. Reste pertinent comme filet de sécurité au-dessus de A, pas comme solution.
  • C n'est pas une option distincte : c'est A avec une capacité fixée dès la conception plutôt qu'ajoutée après coup — bon endroit pour la leçon "borner dès le départ, pas après" (écho possible à la fuite elle-même : un slot unique non borné avait déjà été la cause du bug).
  • E (dynamic shapes automatiques) écarté pour l'instant : suppose une pipeline de lowering avec shapes symboliques, que Sheaf n'a pas encore (lowering/ suppose du statique partout). Raffinement futur sur A, pas maintenant.
  • Point additionnel à ne pas oublier : les scalaires capturés (temperature, etc.) doivent entrer dans la clé de cache aux côtés des shapes — sinon bug de correction silencieux, pas juste de perf. Bon endroit pour une note "ce qu'on a failli rater".

6. Diagramme ASCII #2 — architecture cible (D + A)

                 ┌─────────────────────────────┐
                 │   IreeRuntime (unique, partagé)  │
                 │   instance + device + allocator   │
                 └───────────────┬─────────────┘
                                 │ modules chargés par nom
        ┌────────────────────────┼────────────────────────┐
        │                        │                        │
   module "sigmoid              module "sigmoid            module "softmax
    [1,5]"                       [1,5,1]"                   [B,S,V]"
   (~qq KB, pas de              (~qq KB)                   (~qq KB)
    nouvelle instance)

JitCompiler
  per_function_cache: HashMap<fn_name, LRU<ShapeSignature, ModuleHandle>>
                                          capacité fixe (ex. 8), éviction FIFO/LRU

sigmoid → lookup ShapeSignature{shape, dtype, captured_scalars}
            hit  → dispatch direct
            miss → compile, load module dans le runtime partagé, insert, evict si plein

Légende possible : contraster avec le diagramme #1 — même topologie logique (une fonction, plusieurs shapes possibles), mais le coût par entrée passe de "nouvelle instance de runtime" à "module chargé dans un runtime existant", et le nombre d'entrées passe de non-borné à borné.


Si tu veux, je peux aussi relire un brouillon une fois que tu l'auras rédigé — mais je te laisse la plume, comme demandé.