Marlin / GPTQ-Marlin#

Marlin (Mixed-Precision Auto-Regressive LINear) n'est pas une méthode de quantification — c'est un kernel GPU d'inférence qui rend la quantification GPTQ INT4 pratique et rapide en contexte batché. Il apporte un speedup jusqu'à sur les GEMM mixed-precision et jusqu'à 2.8× end-to-end dans vLLM.


Le problème : GPTQ est précis, mais lent à servir#

La quantification GPTQ (voir la page dédiée) produit des modèles INT4 très compacts avec une perte de qualité minimale. Mais à l'inférence, chaque couche linéaire doit calculer :

    Y = X_fp16 × W_int4

X est une matrice d'activations en FP16 et W est la matrice de poids compressée en INT4. Le GPU ne sait pas multiplier directement du FP16 par de l'INT4 — il faut déquantifier les poids INT4 vers le FP16 à la volée, puis faire la multiplication (GEMM). Ce déquantify-then-GEMM est extrêmement lent avec les kernels naïfs :

flowchart TD W4["W_int4"] --> DQ["DÉQUANT 4→16 bit
⚠️ LENT"] --> W16["W_fp16"] W16 --> GEMM["GEMM FP16"] X16["X_fp16"] --> GEMM GEMM --> Y["Y"]

Problème : la déquantification domine le temps de calcul → la quantification ne speed up RIEN (voire ralentit) en batched serving.

Résultat : sans kernel optimisé, GPTQ INT4 ne speed up l'inférence que pour des batch sizes très petits (1–4). Dès qu'on sert plusieurs requêtes en parallèle (cloud serving, batch 16+), la déquantification devient le bottleneck et INT4 est plus lent que FP16 natif.

C'est ce problème que Marlin résout.


Niveau néophyte : l'analogie#

Imaginez une bibliothèque où les livres sont stockés dans une langue ultra-compressée (abréviations partout). Pour lire un livre, le bibliothécaire doit d'abord le « décompresser » entièrement avant de vous le donner. Si vous êtes seul, ça va. Mais si 32 personnes demandent des livres en même temps, le bibliothécaire est débordé et tout le monde attend.

Marlin, c'est 10 bibliothécaires spécialisés qui travaillent en parallèle : pendant que l'un décompresse la page 2, un autre lit la page 1, un autre prépare la page 3. Personne ne attend — le livre compressé se lit aussi vite qu'un livre normal.


Niveau technique : le kernel Marlin#

Architecture générale#

Marlin est un kernel CUDA single-kernel qui fusionne la déquantification et le GEMM en une seule passe, avec un pipeline soigneusement conçu pour cacher la latence mémoire derrière le calcul.

flowchart LR W4["W_int4
(global mem)"] --> LOAD["LOAD tiles
(async prefetch)"] LOAD --> DEQ["DÉQUANT
INT4→FP16
(registers)"] DEQ --> GEMM["GEMM
FP16 × FP16
(Tensor Cores)"] GEMM --> OUT["Y (output)"] NEXT["NEXT tile
(en attente)"] -.->|"prefetch overlap"| LOAD

KEY : le prefetch du prochain tile chevauche le GEMM du tile courant → la latence mémoire est CACHÉE.

Les 4 innovations clés#

1. Accès mémoire asynchrone (async memcpy)#

gantt title Async memcpy overlap dans Marlin dateFormat s axisFormat %s section Thread block t Tile 0 :a1, 0, 1s Tile 1 :a2, 1s, 1s Tile 2 :a3, 2s, 1s

OVERLAP : le LOAD de Tile 1 commence PENDANT le GEMM de Tile 0. Pendant que le Tensor Core calcule le GEMM du tile courant, la mémoire globale charge déjà le tile suivant. La latence de déquantification est ainsi totalement masquée par le calcul.

Le kernel utilise des prefetch asynchrones : pendant que le Tensor Core calcule le GEMM du tile courant, la mémoire globale charge déjà le tile suivant. La latence de déquantification est ainsi totalement masquée par le calcul.

2. Tiled layout et repacking#

Les poids GPTQ INT4 sont stockés dans un format « group-quantized » (un scale + zero-point par groupe de 128). Ce format n'est pas adapté aux accès mémoire coalesced du GPU.

Marlin effectue un repacking offline des poids dans un layout tiled optimisé :

flowchart LR subgraph BEFORE["AVANT (layout GPTQ natif)"] B1["g0: [w w w w] s0 z0
g1: [w w w w] s1 z1
g2: [w w w w] s2 z2
...
gN: [w w w w] sN zN
Séquentiel par groupe
(mauvais coalescing)"] end subgraph AFTER["APRÈS (layout Marlin)"] A1["Tile(0,0): [w w w w] [s s]
Tile(0,1): [w w w w] [s s]
Tile(1,0): [w w w w] [s s]
Tuiles 2D coalesced
(accès mémoire optimal)"] end BEFORE ==>|"repacking offline
(1 fois au chargement)"| AFTER

Ce repacking se fait une seule fois au chargement du modèle — zéro coût à l'inférence.

3. Task scheduling multi-warp#

Marlin divise le travail entre warps (groupes de 32 threads) avec une stratégie sophistiquée :

flowchart TD TB["Thread block"] --> W0["Warp 0 → Déquant + GEMM tile (0, *)"] TB --> W1["Warp 1 → Déquant + GEMM tile (1, *)"] TB --> W2["Warp 2 → Déquant + GEMM tile (2, *)"] TB --> W3["Warp 3 → Prefetch mémoire (async) pour warps 0-2"]

Pas de stall : chaque warp a toujours du travail prêt.

4. Support de la sparsité 2:4 NVIDIA#

Marlin peut être combiné avec la structured sparsity 2:4 des GPU NVIDIA Ampere+ (2 éléments non-zéros sur 4), doublant virtuellement le débit effectif :

W_int4_sparse (2:4) → Déquant + Skip zeros → GEMM sparse. Moitié des poids = zéro → 2× de calcul utile par cycle.


Pourquoi Marlin est plus rapide que GPTQ natif#

Caractéristique GPTQ natif Marlin
Déquantification Séquentielle (bottleneck) Pipelinée (async, cachée par GEMM)
Layout mémoire Group-quant (non-coalesced) Tiled 2D (repacked, coalesced optimal)
Fusion kernel 2 kernels (dequant+GEMM) 1 kernel fusionné (dequant∥GEMM)
Scheduling Simple Multi-warp + prefetch
Sparsité 2:4 Non supporté Supporté nativement
Batch 1 (single user) ~1× (correct) ~1× (semblable)
Batch 16-32 <1× (plus lent que FP16) ~4× (speedup max !)
Batch 64-128 N/A Accélération notable

Le point clé : Marlin brille surtout en batched serving (cloud), là où GPTQ natif échoue complètement.


Caractéristiques#

Propriété Valeur
Type Kernel d'inférence (pas une méthode de quantification)
Quantification supportée GPTQ INT4 weights × FP16 activations
Précision INT4 poids, FP16 activations (mixed-precision)
GPU requis NVIDIA Ampere+ (A100, A10, L40S, H100, RTX 30xx/40xx)
Speedup GEMM Jusqu'à (batch 16–32)
Speedup end-to-end (vLLM) Jusqu'à 2.8×
Sparsité 2:4 Supportée nativement
Statut Intégré dans vLLM, TensorRT-LLM, et plus

Benchmarks de vitesse#

GEMM seul (matmul INT4×FP16)#

xychart-beta title "Speedup GEMM vs cuBLAS FP16" x-axis ["Batch 1", "Batch 4", "Batch 16", "Batch 32", "Batch 64", "Batch 128"] y-axis "Speedup ×" 0 --> 5 bar [1.0, 2.0, 3.5, 4.0, 3.2, 2.5] line [0.7, 0.8, 0.9, 0.9, 0.9, 0.9]
xychart-beta title "GPTQ naïf vs Marlin (Speedup vs FP16)" x-axis ["Batch 1", "Batch 4", "Batch 16", "Batch 32", "Batch 64", "Batch 128"] y-axis "Speedup ×" 0 --> 5 bar [1.0, 2.0, 3.5, 4.0, 3.2, 2.5]

Marlin atteint le speedup théorique maximal (4×) pour batch 16-32, puis décroît doucement. GPTQ naïf ne speed up JAMAIS le GEMM.

End-to-end dans vLLM (génération complète)#

Modèle Batch vLLM FP16 (tok/s) vLLM + GPTQ-Marlin (tok/s) Speedup
LLaMA-7B 32 ~2 000 ~5 000 2.5×
LLaMA-13B 32 ~1 200 ~3 200 2.7×
LLaMA-70B 16 ~250 ~700 2.8×

Exemple pratique#

Utilisation dans vLLM (le plus simple)#

vLLM détecte automatiquement les modèles GPTQ et utilise Marlin si le GPU le supporte :

# Installer vLLM
pip install vllm

# Servir un modèle GPTQ quantifié — Marlin est activé automatiquement
python -m vllm.entrypoints.openai.api_server \
    --model TheBloke/Llama-2-13B-GPTQ \
    --quantization gptq_marlin \
    --dtype float16 \
    --port 8000

Note : --quantization gptq_marlin force l'utilisation du kernel Marlin. Sans cette option, vLLM peut fallback sur un kernel GPTQ standard selon la configuration.

Vérifier que Marlin est utilisé#

from vllm import LLM, SamplingParams

llm = LLM(
    model="TheBloke/Llama-2-13B-GPTQ",
    quantization="gptq_marlin",
    dtype="float16",
)

# Le log de vLLM affiche :
# "Using MarlinLinearKernel for ..." si Marlin est actif

Conversion d'un modèle GPTQ existant vers le format Marlin#

from vllm.model_executor.layers.quantization.gptq import GPTQConfig

# Le repacking (INT4 → Marlin tiled layout) est automatique
# au chargement du modèle dans vLLM. Aucune étape manuelle.

Benchmark comparatif#

# Avec vLLM benchmark tool
python -m vllm.benchmark.benchmark_serving \
    --model TheBloke/Llama-2-13B-GPTQ \
    --quantization gptq_marlin \
    --num-prompts 100 \
    --request-rate 10

# Comparer avec :
python -m vllm.benchmark.benchmark_serving \
    --model meta-llama/Llama-2-13b-hf \
    --num-prompts 100 \
    --request-rate 10

Limitations#

  1. Pas une méthode de quantification — Marlin accélère l'inférence de modèles déjà quantifiés en GPTQ INT4 (ou AWQ avec adaptation).
  2. Configuration fixe — optimisé pour INT4 weights × FP16 activations. Pas de support natif pour INT8 ou d'autres configurations.
  3. Complexité CUDA avancée — le kernel est du CUDA hautement optimisé, difficile à modifier ou porter vers d'autres architectures (AMD/ROCm).
  4. Décroissance pour batch > 64 — le speedup diminue aux très grands batch sizes car le GEMM devient compute-bound (et non plus memory-bound).
  5. GPU Ampere+ requis — ne fonctionne pas sur les GPU plus anciens (V100, Pascal).

Références#

ia llm quantification marlin gptq-marlin kernel inference gpu cuda vllm int4