boot.py 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415
  1. """Pinned checkpoint download, integrity verification, serving and API acceptance.
  2. Spark remap of 0xSero/deepseek-v4.1-flash-4x-rtx-pro-6000:
  3. 4x RTX PRO 6000 TP4/EP4 -> 3x DGX Spark (GB10) TP3/EP3 over NCCL.
  4. Engram tables stay on NVMe (or SSHFS) via the row-store adapter. Do not use
  5. RAM mode on Spark: host RAM is the GPU's unified memory.
  6. """
  7. import concurrent.futures
  8. import base64
  9. import io
  10. import hashlib
  11. import json
  12. import os
  13. from pathlib import Path
  14. import secrets
  15. import signal
  16. import subprocess
  17. import sys
  18. import threading
  19. import time
  20. import urllib.request
  21. REPO = 'deepseek-ai/DeepSeek-V4.1-Flash'
  22. REVISION = 'fb2764a5cf321eaa5070ca8f9e892818f477c16d'
  23. MODEL = Path(os.environ.get('MODEL_PATH', '/models/DeepSeek-V4.1-Flash'))
  24. STATE = Path(os.environ.get('STATE_PATH', '/state'))
  25. PORT = int(os.environ.get('SERVER_PORT', '8888'))
  26. NNODES = int(os.environ.get('NNODES', '1'))
  27. NODE_RANK = int(os.environ.get('NODE_RANK', '0'))
  28. TP_SIZE = int(os.environ.get('TP_SIZE', os.environ.get('TP', '3')))
  29. EP_SIZE = int(os.environ.get('EP_SIZE', str(TP_SIZE)))
  30. DIST_INIT_ADDR = os.environ.get('DIST_INIT_ADDR', '')
  31. SERVED_MODEL_NAME = os.environ.get('SERVED_MODEL_NAME', 'deepseek-v4.1-flash')
  32. def save(name, value):
  33. STATE.mkdir(parents=True, exist_ok=True)
  34. temporary = STATE / (name + '.tmp')
  35. temporary.write_text(json.dumps(value, indent=2))
  36. temporary.replace(STATE / name)
  37. def prepare():
  38. if os.environ.get('SKIP_PREPARE', '0') == '1':
  39. assert (MODEL / 'config.json').is_file(), f'missing {MODEL}/config.json'
  40. print('SKIP_PREPARE=1 — using existing checkpoint', flush=True)
  41. return
  42. if NODE_RANK != 0:
  43. deadline = time.monotonic() + 3600
  44. while time.monotonic() < deadline:
  45. if (MODEL / 'config.json').is_file():
  46. print(f'Rank {NODE_RANK}: checkpoint visible at {MODEL}', flush=True)
  47. return
  48. time.sleep(5)
  49. raise RuntimeError(f'Rank {NODE_RANK}: {MODEL} never appeared')
  50. from huggingface_hub import snapshot_download
  51. MODEL.mkdir(parents=True, exist_ok=True)
  52. metadata = json.load(urllib.request.urlopen(
  53. f'https://huggingface.co/api/models/{REPO}/revision/{REVISION}?blobs=true', timeout=60))
  54. assert metadata['sha'] == REVISION
  55. snapshot_download(REPO, revision=REVISION, local_dir=str(MODEL), max_workers=4)
  56. if os.environ.get('SKIP_VERIFY', '0') == '1':
  57. print('SKIP_VERIFY=1 — not re-hashing the checkpoint', flush=True)
  58. return
  59. receipt = STATE / 'verification.json'
  60. old = json.loads(receipt.read_text()) if receipt.exists() else {}
  61. cached = {f['name']: f for f in old.get('files', [])} if old.get('revision') == REVISION else {}
  62. def verify(entry):
  63. name = entry['rfilename']
  64. path = MODEL / name
  65. assert path.resolve().is_relative_to(MODEL.resolve())
  66. before = path.stat()
  67. identity = {'bytes': before.st_size, 'mtime_ns': before.st_mtime_ns, 'inode': before.st_ino}
  68. assert before.st_size == entry['size'], f'Wrong file size: {name}'
  69. expected = entry['lfs']['sha256'] if entry.get('lfs') else entry['blobId']
  70. previous = cached.get(name, {})
  71. if previous.get('digest') == expected and all(previous.get(k) == v for k, v in identity.items()):
  72. return previous
  73. digest = hashlib.sha256() if entry.get('lfs') else hashlib.sha1()
  74. if not entry.get('lfs'):
  75. digest.update(f'blob {before.st_size}\0'.encode())
  76. with path.open('rb') as handle:
  77. while block := handle.read(8 * 1024 * 1024):
  78. digest.update(block)
  79. assert digest.hexdigest() == expected, f'Hash mismatch: {name}'
  80. after = path.stat()
  81. assert (before.st_size, before.st_mtime_ns, before.st_ino) == (after.st_size, after.st_mtime_ns, after.st_ino)
  82. print('Verified', name, flush=True)
  83. return {'name': name, 'digest': expected, **identity}
  84. with concurrent.futures.ThreadPoolExecutor(4) as pool:
  85. files = list(pool.map(verify, metadata['siblings']))
  86. verified = {f['name'] for f in files}
  87. index = json.loads((MODEL / 'model.safetensors.index.json').read_text())['weight_map']
  88. assert set(index.values()) <= verified
  89. save('verification.json', {'revision': REVISION, 'status': 'verified', 'files': files})
  90. def key():
  91. value = os.environ.get('API_KEY', '').strip()
  92. if value.lower() in ('', 'none', 'off', 'dummy', '0'):
  93. return ''
  94. STATE.mkdir(parents=True, exist_ok=True)
  95. path = STATE / 'api-key'
  96. path.write_text(value)
  97. path.chmod(0o600)
  98. return value
  99. def request(path, payload=None, timeout=10):
  100. headers = {'Content-Type': 'application/json'}
  101. secret = key()
  102. if secret:
  103. headers['Authorization'] = 'Bearer ' + secret
  104. req = urllib.request.Request(
  105. f'http://127.0.0.1:{PORT}' + path,
  106. headers=headers,
  107. data=None if payload is None else json.dumps(payload).encode())
  108. with urllib.request.urlopen(req, timeout=timeout) as response:
  109. body = response.read()
  110. return json.loads(body) if body else {'status': response.status}
  111. def smoke():
  112. result = request('/v1/chat/completions', dict(
  113. model=SERVED_MODEL_NAME, temperature=0, max_tokens=32,
  114. chat_template_kwargs={'thinking': False},
  115. messages=[dict(role='user', content='What is 19 + 23? Reply only with the number.')]),
  116. timeout=300)
  117. choice = result['choices'][0]
  118. assert choice['message']['content'].strip() == '42' and choice['finish_reason'] == 'stop', result
  119. save('smoke.json', result)
  120. print('Fresh inference passed: 19 + 23 = 42', flush=True)
  121. if os.environ.get('SMOKE_QUICK', '0') == '1':
  122. return
  123. common = dict(model=SERVED_MODEL_NAME, temperature=0, max_tokens=256,
  124. chat_template_kwargs={'thinking': False})
  125. structured = request('/v1/chat/completions', dict(common,
  126. messages=[dict(role='user', content='Return an object whose answer is the integer 42.')],
  127. response_format={'type': 'json_schema', 'json_schema': {'name': 'answer', 'strict': True,
  128. 'schema': {'type': 'object', 'properties': {'answer': {'type': 'integer'}},
  129. 'required': ['answer'], 'additionalProperties': False}}}), timeout=300)
  130. assert json.loads(structured['choices'][0]['message']['content']) == {'answer': 42}
  131. save('smoke-structured.json', structured)
  132. tool = {'type': 'function', 'function': {'name': 'lookup_fixture',
  133. 'description': 'Retrieve a stored test value.',
  134. 'parameters': {'type': 'object', 'properties': {'key': {'type': 'string'}},
  135. 'required': ['key'], 'additionalProperties': False}}}
  136. messages = [dict(role='user', content='Use lookup_fixture to retrieve the value for key alpha. Do not guess.')]
  137. called = request('/v1/chat/completions', dict(common, messages=messages, tools=[tool]), timeout=300)
  138. assistant = called['choices'][0]['message']
  139. calls = assistant.get('tool_calls') or []
  140. assert len(calls) == 1 and calls[0]['function']['name'] == 'lookup_fixture', called
  141. assert json.loads(calls[0]['function']['arguments']) == {'key': 'alpha'}
  142. messages += [assistant, dict(role='tool', tool_call_id=calls[0]['id'], content='{"value":42}')]
  143. continued = request('/v1/chat/completions', dict(common, messages=messages, tools=[tool]), timeout=300)
  144. assert '42' in continued['choices'][0]['message']['content'], continued
  145. save('smoke-tools.json', {'call': called, 'continuation': continued})
  146. from PIL import Image, ImageDraw
  147. image = Image.new('RGB', (3024, 588), 'white')
  148. draw = ImageDraw.Draw(image)
  149. draw.ellipse((200, 100, 588, 488), fill='red')
  150. draw.rectangle((2400, 100, 2788, 488), fill='blue')
  151. buffer = io.BytesIO()
  152. image.save(buffer, format='PNG')
  153. visual = request('/v1/chat/completions', dict(common, messages=[dict(role='user', content=[
  154. {'type': 'text', 'text': 'Describe the two colored shapes and their left-to-right order. Be concise.'},
  155. {'type': 'image_url', 'image_url': {'url': 'data:image/png;base64,' + base64.b64encode(buffer.getvalue()).decode()}}
  156. ])]), timeout=300)
  157. text = visual['choices'][0]['message']['content'].lower()
  158. assert all(word in text for word in ('red', 'circle', 'blue', 'square')), visual
  159. details = (visual.get('usage') or {}).get('prompt_tokens_details') or {}
  160. if 'image_tokens' in details:
  161. assert details['image_tokens'] == 1024, visual
  162. save('smoke-vision.json', visual)
  163. print('Structured output, tool round trip and full-budget native vision passed.', flush=True)
  164. def warmup():
  165. """Exercise the kernels a fresh engine has not seen yet, so the first real
  166. requests do not pay for JIT compiles, autotune buckets or first-touch page
  167. allocation: prompts of a few sizes (prefill tiers), a decode run, and one
  168. concurrent batch of MAX_RUNNING_REQUESTS. Failures are logged, not fatal."""
  169. if os.environ.get('WARMUP', '1') in ('0', 'off', 'false'):
  170. return
  171. started = time.monotonic()
  172. common = dict(model=SERVED_MODEL_NAME, temperature=0,
  173. chat_template_kwargs={'thinking': False})
  174. filler = ('The quick brown fox jumps over the lazy dog near the riverbank '
  175. 'while the sun sets slowly behind the distant hills. ')
  176. sizes = [int(x) for x in os.environ.get('WARMUP_PROMPT_WORDS', '12,200,900,3600').split(',')]
  177. for words in sizes:
  178. prompt = (filler * (words // 20 + 1)) + 'Summarise the text above in one sentence.'
  179. try:
  180. request('/v1/chat/completions', dict(common, max_tokens=24,
  181. messages=[dict(role='user', content=prompt)]), timeout=600)
  182. except Exception as exc: # warm-up must never take the engine down
  183. print(f'warm-up prompt (~{words} words) failed: {exc}', flush=True)
  184. batch = max(1, int(os.environ.get('MAX_RUNNING_REQUESTS', '4')))
  185. prompts = ['Explain how a hash table handles collisions in two sentences.',
  186. 'List the steps of the TCP three-way handshake.',
  187. 'Describe a quiet morning in a small harbour town in two sentences.',
  188. 'Explain in plain words why the sky is blue.'] * ((batch + 3) // 4)
  189. # Sanity check on the batch: every request of a MAX_RUNNING_REQUESTS batch must
  190. # come back as Latin prose. On 2026-09-11 a boot (chunk 1024 / pool 750k / ctx 256k)
  191. # returned symbols and CJK for every request but the last of any greedy batch of
  192. # 3-4 (NaN target logits at bs >= 3) while single requests were fine, so the
  193. # arithmetic smoke alone cannot catch it. Loud warning, not fatal.
  194. with concurrent.futures.ThreadPoolExecutor(batch) as pool:
  195. outcomes = list(pool.map(lambda p: _try_request(dict(common, max_tokens=48,
  196. messages=[dict(role='user', content=p)])), prompts[:batch]))
  197. garbled = []
  198. for prompt, (error, text) in zip(prompts[:batch], outcomes):
  199. if error:
  200. print(f'warm-up batch request failed: {error}', flush=True)
  201. elif not _looks_like_latin_prose(text):
  202. garbled.append((prompt, text))
  203. if garbled:
  204. print(f'WARNING: BATCH OUTPUT SANITY CHECK FAILED: {len(garbled)}/{batch} requests of a '
  205. f'batch of {batch} (MAX_RUNNING_REQUESTS) did not return Latin prose. '
  206. 'Concurrent requests at this batch size are returning garbage '
  207. '(see logs/profile-2026-09-10/REPORT.md section 16).', flush=True)
  208. for prompt, text in garbled:
  209. print(f' batch of {batch}: {prompt[:40]!r} -> {text[:80]!r}', flush=True)
  210. else:
  211. print(f'Batch output sanity check passed: batch of {batch} returned Latin prose.',
  212. flush=True)
  213. print(f'Warm-up done in {time.monotonic() - started:.0f}s '
  214. f'({len(sizes)} prompt sizes + one batch of {batch})', flush=True)
  215. def _looks_like_latin_prose(text):
  216. """True when the text reads as Latin-script prose: no CJK / Hangul, and at
  217. least 70% of the non-space characters are ASCII letters, digits or
  218. common punctuation. The garbage seen at bs >= 3 was symbol runs and CJK."""
  219. body = [ch for ch in (text or '') if not ch.isspace()]
  220. if not body:
  221. return False
  222. if any('\u3000' <= ch <= '\u9fff' or '\uac00' <= ch <= '\ud7af' for ch in body):
  223. return False
  224. latin = sum(1 for ch in body if ch.isascii() and (ch.isalnum() or ch in '.,;:\'"!?-()'))
  225. return latin / len(body) >= 0.7
  226. def _try_request(payload):
  227. """(error, text): error is None on success, text is the assistant content."""
  228. try:
  229. result = request('/v1/chat/completions', payload, timeout=600)
  230. return None, result['choices'][0]['message']['content']
  231. except Exception as exc:
  232. return str(exc), ''
  233. def _gpu_lines():
  234. return subprocess.check_output(
  235. ['nvidia-smi', '--query-gpu=name,memory.total', '--format=csv,noheader,nounits'],
  236. text=True).strip().splitlines()
  237. def serve():
  238. mode = os.environ.get('OFFLOAD_MODE', 'nvme')
  239. assert mode in ('nvme', 'ram'), 'OFFLOAD_MODE must be nvme or ram'
  240. context = int(os.environ.get('CONTEXT_LENGTH', '409600'))
  241. assert 4096 <= context <= 1048576, 'Context must be 4096 through the model limit 1048576'
  242. gpu = _gpu_lines()
  243. assert gpu, gpu
  244. if mode == 'ram':
  245. if os.environ.get('ALLOW_RAM_OFFLOAD', '0') != '1':
  246. raise RuntimeError(
  247. 'RAM Engram offload pins ~189 GiB into host RAM, which on DGX Spark '
  248. 'is the same pool as the GPU. Use OFFLOAD_MODE=nvme (default). '
  249. 'Set ALLOW_RAM_OFFLOAD=1 to override.')
  250. index = json.loads((MODEL / 'model.safetensors.index.json').read_text())['weight_map']
  251. shards = {index[f'layers.{layer}.engram.embed.weight'] for layer in (1, 14)}
  252. needed = sum((MODEL / name).stat().st_size for name in shards) + 24 * 2**30
  253. mem = dict(line.split(':', 1) for line in Path('/proc/meminfo').read_text().splitlines())
  254. available = int(mem['MemAvailable'].split()[0]) * 1024
  255. assert available >= needed, (
  256. f'RAM offload needs at least {needed / 2**30:.1f} GiB available; '
  257. f'found {available / 2**30:.1f}. Use nvme mode.')
  258. mem_frac = os.environ.get('MEM_FRACTION_STATIC', '0.95')
  259. chunk = os.environ.get('CHUNKED_PREFILL_SIZE', '2048')
  260. max_req = os.environ.get('MAX_RUNNING_REQUESTS', '4')
  261. graph_bs = os.environ.get('CUDA_GRAPH_MAX_BS_DECODE', max_req)
  262. spec = os.environ.get('SPEC_ALGO', 'DSPARK')
  263. args = [
  264. '--model-path', str(MODEL),
  265. '--served-model-name', SERVED_MODEL_NAME,
  266. '--trust-remote-code', '--load-format', 'safetensors',
  267. '--tp', str(TP_SIZE), '--ep-size', str(EP_SIZE),
  268. '--attention-backend', os.environ.get('ATTENTION_BACKEND', 'dsv4'),
  269. '--moe-runner-backend', os.environ.get('MOE_RUNNER_BACKEND', 'flashinfer_mxfp4'),
  270. '--mem-fraction-static', mem_frac,
  271. '--chunked-prefill-size', chunk,
  272. '--context-length', str(context),
  273. '--max-running-requests', max_req,
  274. '--cuda-graph-max-bs-decode', graph_bs,
  275. '--random-seed', '0',
  276. '--enable-decoder-swa-bounded-replay',
  277. '--enable-cache-report',
  278. '--tool-call-parser', os.environ.get('TOOL_CALL_PARSER', 'deepseekv41'),
  279. '--reasoning-parser', os.environ.get('REASONING_PARSER', 'deepseek-v41'),
  280. '--host', os.environ.get('HOST', '0.0.0.0'),
  281. '--port', str(PORT),
  282. ]
  283. if spec and spec.lower() not in ('off', 'none', '0'):
  284. args += [
  285. '--speculative-algorithm', spec,
  286. '--speculative-dspark-block-size', os.environ.get('DSPARK_BLOCK_SIZE', '5'),
  287. ]
  288. # The ragged-verify scheduler is where DSpark's throughput actually comes
  289. # from, and it needs a profiled SPS cost table: without one the planner
  290. # falls back to verify-all and the whole thing is a no-op. Build one with
  291. # python -m sglang.benchmark.dspark_sps_profiler
  292. # against a running server, then point DSPARK_SPS_TABLE at it.
  293. table = os.environ.get('DSPARK_SPS_TABLE', '').strip()
  294. if table and Path(table).is_file():
  295. args += ['--speculative-dspark-sps-table-path', table]
  296. # Fills each step's verify window up to the cuda-graph tier the
  297. # forward is padded to anyway: free verification at the same cost.
  298. args += ['--speculative-dspark-align-verify-tokens-to-graph-tier']
  299. elif table:
  300. print(f'DSPARK_SPS_TABLE={table} not found; '
  301. 'staying on the verify-all schedule', flush=True)
  302. sts = os.environ.get('DSPARK_STS_TABLE', '').strip()
  303. if sts and Path(sts).is_file():
  304. args += ['--speculative-dspark-confidence-sts-path', sts]
  305. if NNODES > 1:
  306. assert DIST_INIT_ADDR, 'DIST_INIT_ADDR is required when NNODES>1'
  307. args += [
  308. '--nnodes', str(NNODES),
  309. '--node-rank', str(NODE_RANK),
  310. '--dist-init-addr', DIST_INIT_ADDR,
  311. ]
  312. # Pin the KV pool. Left to itself SGLang sizes it from whatever happens to be
  313. # free at load time, and it varies by ~2x between boots -- on unified memory
  314. # the bigger pool comes straight out of host RAM and the head starts stalling
  315. # on reclaim, which costs more throughput than the extra KV ever buys.
  316. total_tokens = os.environ.get('MAX_TOTAL_TOKENS', '').strip()
  317. if total_tokens and total_tokens != '0':
  318. args += ['--max-total-tokens', total_tokens]
  319. extra = os.environ.get('EXTRA_SGLANG_ARGS', '').strip()
  320. if extra:
  321. args += extra.split()
  322. save('launch.json', {
  323. 'revision': REVISION, 'offload_mode': mode, 'args': args, 'gpus': gpu,
  324. 'nnodes': NNODES, 'node_rank': NODE_RANK, 'tp': TP_SIZE, 'ep': EP_SIZE,
  325. })
  326. secret = key()
  327. env = dict(os.environ, DSV41_SOURCE=str(MODEL))
  328. # compact ragged-verify only pays off with a profiled cost table; without one
  329. # it degenerates to the same verify-all schedule as static, for more work.
  330. if '--speculative-dspark-sps-table-path' in args:
  331. env.setdefault('SGLANG_RAGGED_VERIFY_MODE', 'compact')
  332. cmd = [sys.executable, '-m', 'sglang.launch_server', *args]
  333. if secret:
  334. cmd += ['--api-key', secret]
  335. process = subprocess.Popen(
  336. cmd, env=env, stdout=subprocess.PIPE, stderr=subprocess.STDOUT,
  337. text=True, start_new_session=True)
  338. def stop(*_):
  339. if process.poll() is None:
  340. os.killpg(process.pid, signal.SIGTERM)
  341. signal.signal(signal.SIGTERM, stop)
  342. signal.signal(signal.SIGINT, stop)
  343. def logs():
  344. for line in process.stdout:
  345. print(line.replace(secret, '[REDACTED]') if secret else line, end='', flush=True)
  346. threading.Thread(target=logs, daemon=True).start()
  347. try:
  348. if NODE_RANK != 0:
  349. return process.wait()
  350. deadline = time.monotonic() + int(os.environ.get('READY_TIMEOUT_S', '3600'))
  351. while process.poll() is None and time.monotonic() < deadline:
  352. try:
  353. request('/health', timeout=3)
  354. break
  355. except Exception:
  356. time.sleep(5)
  357. else:
  358. raise RuntimeError('Server failed to become healthy; inspect container logs')
  359. if os.environ.get('SKIP_SMOKE', '0') != '1':
  360. smoke()
  361. warmup()
  362. if secret:
  363. print(f'Ready: API on port {PORT} (rank 0). Key is in {STATE}/api-key.', flush=True)
  364. else:
  365. print(f'Ready: API on port {PORT} (rank 0), no API key.', flush=True)
  366. return process.wait()
  367. finally:
  368. stop()
  369. if __name__ == '__main__':
  370. command = sys.argv[1] if len(sys.argv) > 1 else 'run'
  371. if command == 'health':
  372. request('/health')
  373. elif command == 'smoke':
  374. smoke()
  375. elif command == 'prepare':
  376. prepare()
  377. elif command == 'run':
  378. prepare()
  379. sys.exit(serve())
  380. else:
  381. raise SystemExit('Commands: run, prepare, health, smoke')