patdev commited on
Commit
cd1dd3a
·
verified ·
1 Parent(s): 208a867

backends MoE sur Hopper : marlin, humming, trtllm, cutlass

Browse files
Files changed (1) hide show
  1. banc_backends_moe_hopper.py +227 -0
banc_backends_moe_hopper.py ADDED
@@ -0,0 +1,227 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Forcer chaque backend MoE NVFP4 sur sm_120, au lieu de subir le choix de vLLM.
2
+
3
+ Le journal de vLLM affiche :
4
+
5
+ Using 'MARLIN' NvFp4 MoE backend out of potential backends:
6
+ ['FLASHINFER_TRTLLM', 'FLASHINFER_CUTEDSL', 'FLASHINFER_CUTEDSL_BATCHED',
7
+ 'FLASHINFER_CUTLASS', 'VLLM_CUTLASS', 'MARLIN', 'HUMMING', 'EMULATION']
8
+
9
+ Cette liste est l'ENUMERATION des valeurs possibles, pas la liste des backends
10
+ compatibles avec la carte. vLLM enumere tout, filtre selon la capacite, et
11
+ retient Marlin -- sans jamais dire pourquoi il ecarte les autres.
12
+
13
+ Or toute la conclusion de la campagne tient en une phrase : « le goulot est la
14
+ dequantification de Marlin, pas la bande passante ». Cette phrase se teste en
15
+ SORTANT de Marlin. On ne l'avait jamais tente, seulement observe passivement.
16
+
17
+ Chaque backend est donc force explicitement. Trois issues possibles, et les
18
+ trois sont informatives :
19
+ - il demarre et va plus vite -> la conclusion Marlin tombe, il faut la reecrire ;
20
+ - il demarre et va moins vite -> Marlin est bien le meilleur disponible ici ;
21
+ - il refuse de demarrer -> on capture la raison exacte, qui manquait.
22
+
23
+ Contexte reduit a 131 072 volontairement : on compare des backends entre eux
24
+ dans une meme execution, pas a la reference 1 M. Ca divise par plusieurs le
25
+ temps d'allocation du cache et de capture des graphes, donc le cout.
26
+ """
27
+ import json
28
+ import statistics
29
+ import subprocess
30
+ import threading
31
+ import time
32
+ import urllib.request
33
+
34
+ MODEL = "nvidia/NVIDIA-Nemotron-3.5-Lightning-30B-A3B-NVFP4"
35
+ PORT = 8000
36
+ URL = "http://127.0.0.1:%d" % PORT
37
+
38
+ # Liste reduite pour Hopper : quatre essais au lieu de sept, parce que la H200
39
+ # coute 5 $/h et que chaque demarrage vaut ~4 minutes. On garde :
40
+ # marlin -- la reference, le seul qui tourne partout ;
41
+ # humming -- ce que NVIDIA prescrit pour le DEBIT sur H100 :
42
+ # "1x H100 - max throughput (batch) : vLLM, no spec
43
+ # decoding (humming backend)". Jamais essaye sur sm_90 ;
44
+ # flashinfer_trtllm -- refuse sur sm_120 pour "kernel does not support current
45
+ # device" : ses noyaux visent sm_100/sm_90, donc ici ils
46
+ # devraient passer ;
47
+ # flashinfer_cutlass-- refuse sur sm_120 pour le SCHEMA de quantification
48
+ # (u8 + echelles f8e4m3), pas pour la carte : on saura
49
+ # si ce refus est materiel ou lie au depot.
50
+ BACKENDS = ["marlin", "humming", "flashinfer_trtllm", "flashinfer_cutlass"]
51
+
52
+
53
+ def dire(*a):
54
+ print(*a, flush=True)
55
+
56
+
57
+ dire("=" * 72)
58
+ subprocess.run(["nvidia-smi", "--query-gpu=name,memory.total,compute_cap",
59
+ "--format=csv,noheader"], check=False)
60
+ subprocess.run(["python3", "-c",
61
+ "import vllm,torch;print('vllm',vllm.__version__,'torch',torch.__version__,"
62
+ "'cap',torch.cuda.get_device_capability(0))"], check=False)
63
+ dire("=" * 72)
64
+
65
+ BASE = ["vllm", "serve", MODEL,
66
+ "--served-model-name", "ornith",
67
+ "--host", "127.0.0.1", "--port", str(PORT),
68
+ "--trust-remote-code",
69
+ "--max-model-len", "131072",
70
+ "--kv-cache-dtype", "fp8",
71
+ "--enable-prefix-caching",
72
+ "--gpu-memory-utilization", "0.85",
73
+ "--mamba-backend", "flashinfer",
74
+ "--mamba-cache-mode", "align",
75
+ "--reasoning-parser", "nemotron_v3",
76
+ "--tool-call-parser", "qwen3_coder",
77
+ "--enable-auto-tool-choice"]
78
+
79
+ SUJETS = ["un cache LRU avec dict et liste doublement chainee",
80
+ "un pool de connexions avec expiration et sante des sockets",
81
+ "un analyseur d'expressions arithmetiques par descente recursive",
82
+ "une file de priorite par tas binaire avec decrease-key"]
83
+
84
+
85
+ def demarrer(backend, journal):
86
+ with open(journal, "w") as f:
87
+ p = subprocess.Popen(BASE + ["--moe-backend", backend],
88
+ stdout=f, stderr=subprocess.STDOUT)
89
+ for i in range(75):
90
+ try:
91
+ urllib.request.urlopen(URL + "/v1/models", timeout=5).read()
92
+ return p, i * 10
93
+ except Exception:
94
+ pass
95
+ if p.poll() is not None:
96
+ return None, i * 10
97
+ time.sleep(10)
98
+ p.terminate()
99
+ return None, 750
100
+
101
+
102
+ def une(sujet, res, i):
103
+ corps = json.dumps({
104
+ "model": "ornith",
105
+ "messages": [{"role": "user",
106
+ "content": "Ecris en Python %s, avec trois tests unittest." % sujet}],
107
+ "max_tokens": 300, "temperature": 0.0, "stream": True}).encode()
108
+ r = urllib.request.Request(URL + "/v1/chat/completions", data=corps,
109
+ headers={"Content-Type": "application/json"})
110
+ t1 = None
111
+ n = 0
112
+ bouts = []
113
+ try:
114
+ with urllib.request.urlopen(r, timeout=600) as rep:
115
+ for l in rep:
116
+ l = l.strip()
117
+ if not l.startswith(b"data: ") or l[6:] == b"[DONE]":
118
+ continue
119
+ ch = (json.loads(l[6:]).get("choices") or [{}])[0]
120
+ de = ch.get("delta", {}) or {}
121
+ x = de.get("content") or de.get("reasoning") or de.get("reasoning_content")
122
+ if x:
123
+ if t1 is None:
124
+ t1 = time.time()
125
+ n += 1
126
+ bouts.append(x)
127
+ except Exception as e:
128
+ res[i] = {"err": "%s: %s" % (type(e).__name__, str(e)[:70])}
129
+ return
130
+ res[i] = {"n": n, "t1": t1, "t2": time.time(), "txt": "".join(bouts)}
131
+
132
+
133
+ def div4(t):
134
+ m = t.split()
135
+ if len(m) < 40:
136
+ return 1.0
137
+ g = [tuple(m[i:i + 4]) for i in range(len(m) - 3)]
138
+ return len(set(g)) / len(g)
139
+
140
+
141
+ def mesurer(conc):
142
+ res = [None] * conc
143
+ d0 = time.time()
144
+ fils = [threading.Thread(target=une, args=(SUJETS[i % len(SUJETS)], res, i))
145
+ for i in range(conc)]
146
+ for f in fils:
147
+ f.start()
148
+ for f in fils:
149
+ f.join()
150
+ d1 = time.time()
151
+ bons = [r for r in res if r and not r.get("err") and r.get("t1")]
152
+ if not bons:
153
+ return None, None, None
154
+ tot = sum(r["n"] for r in bons)
155
+ pf = statistics.median([(r["n"] - 1) / (r["t2"] - r["t1"])
156
+ for r in bons if r["t2"] > r["t1"]])
157
+ dv = statistics.median([div4(r["txt"]) for r in bons])
158
+ return tot / (d1 - d0), pf, dv
159
+
160
+
161
+ resume = []
162
+ for idx, backend in enumerate(BACKENDS):
163
+ dire("\n" + "=" * 72)
164
+ dire("%d. --moe-backend %s" % (idx + 1, backend))
165
+ dire("=" * 72)
166
+ journal = "/tmp/moe_%s.log" % backend
167
+ proc, secondes = demarrer(backend, journal)
168
+ texte = open(journal, errors="replace").read()
169
+
170
+ retenu = None
171
+ for ligne in texte.splitlines():
172
+ if "NvFp4 MoE backend" in ligne:
173
+ retenu = ligne.split("Using")[-1].split("NvFp4")[0].strip().strip("'")
174
+ dire(" backend effectivement retenu : %s" % retenu)
175
+ break
176
+ for motif in ("NvFp4LinearKernel", "GPU KV cache size"):
177
+ for ligne in texte.splitlines():
178
+ if motif in ligne:
179
+ dire(" " + ligne.split("] ")[-1][:150])
180
+ break
181
+
182
+ if not proc:
183
+ dire(" NE DEMARRE PAS (%d s)" % secondes)
184
+ # La raison exacte du refus est le vrai produit de cet essai.
185
+ vu = set()
186
+ for ligne in texte.splitlines():
187
+ if any(m in ligne for m in ("RuntimeError", "ValueError", "Traceback",
188
+ "NotImplementedError", "AssertionError",
189
+ "is not supported", "not supported on",
190
+ "Unsupported", "requires", "ImportError",
191
+ "ModuleNotFoundError", "capability")):
192
+ t = ligne.split("] ")[-1][:170]
193
+ if t not in vu:
194
+ vu.add(t)
195
+ dire(" > " + t)
196
+ if len(vu) >= 6:
197
+ break
198
+ resume.append((backend, retenu, None, None))
199
+ continue
200
+
201
+ dire(" PRET en %d s" % secondes)
202
+ dire("conc | agrege | par flux | 4-gr")
203
+ solo = None
204
+ for conc in (1, 4):
205
+ ag, pf, dv = mesurer(conc)
206
+ if ag is None:
207
+ dire("%4d | ECHEC" % conc)
208
+ continue
209
+ if conc == 1:
210
+ solo = pf
211
+ dire("%4d | %8.1f | %8.1f | %.3f %s"
212
+ % (conc, ag, pf, dv, "" if dv > 0.6 else " DEGENERE"))
213
+ resume.append((backend, retenu, solo, None))
214
+ proc.terminate()
215
+ time.sleep(25)
216
+
217
+ dire("\n" + "=" * 72)
218
+ dire("RESUME -- backends MoE NVFP4 sur cette carte")
219
+ dire("=" * 72)
220
+ dire("%-28s %-14s %10s" % ("demande", "retenu", "solo"))
221
+ for backend, retenu, solo, _ in resume:
222
+ dire("%-28s %-14s %10s"
223
+ % (backend, retenu or "-", ("%.1f" % solo) if solo else "ne demarre pas"))
224
+ dire("\nUn backend 'retenu' different du 'demande' signifie que vLLM a ignore le")
225
+ dire("drapeau et refait son propre choix : dans ce cas le chiffre n'est PAS")
226
+ dire("celui du backend demande, et ne prouve rien sur lui.")
227
+ dire("\ncontexte 131 072 ici : comparaison entre backends, pas avec la mesure 1 M.")