autotune_keep.py 6.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126
  1. """Keep the FlashInfer autotune cache across boots under expert parallelism. Default OFF.
  2. DSV41_AUTOTUNE_KEEP=1. The engine's entry gate (_drop_diverged_autotune_cache) digests each rank's
  3. whole cache file and drops every cache when the digests differ. Under EP each rank tunes its own
  4. MoE shapes, so the files can never agree: the cache is deleted on every boot and the fused-MoE
  5. tactics are re-drawn from timing noise (sglang#40320, fix proposed in sglang#40420).
  6. Here the digest covers only the load decision, as in #40420, plus one guard of our own: a sidecar
  7. fingerprint of the launch (argv and the SGLANG_/DSV41_/SPARK_/B12X_/NCCL_ environment) written next
  8. to the cache after tuning. A cache is kept only when its sidecar matches the current launch, so a
  9. config change cannot leave one rank with a partial hit (a hit skips a profile, and a skipped
  10. profile on one rank desynchronises the timing reduction).
  11. """
  12. import hashlib
  13. import json
  14. import os
  15. import sys
  16. from pathlib import Path
  17. ENABLED = os.environ.get("DSV41_AUTOTUNE_KEEP", "0").strip() not in ("0", "", "off", "false")
  18. _PREFIXES = ("SGLANG_", "DSV41_", "SPARK_", "B12X_", "NCCL_")
  19. # Switches that change neither the GEMM shapes nor the kernels tuned: A/B arms over them share tactics.
  20. # ./start.sh keeps this set. The extra names are volatile only on start-tp4.sh: they are
  21. # not set on the TP3 profile, and treating them as volatile there would change nothing,
  22. # but a TP3 boot must keep the fingerprint this file had before the TP4 line.
  23. _VOLATILE = ("DSV41_DRAFT_CAPTURE", "DSV41_DRAFT_CAPTURE_OUT", "DSV41_DRAFT_CAPTURE_TRIGGER",
  24. "DSV41_DRAFT_CAPTURE_MAX_GIB", "DSV41_VERIFY_CAP", "DSV41_VERIFY_CAP_MIN", "DSV41_DRAFT_TAU",
  25. "DSV41_BLOCK_VERIFY", "DSV41_FOLDED_FENCE", "DSV41_AUTOTUNE_KEEP",
  26. # TP3 additions: neither touches a FlashInfer-tuned op (the draft LM head is a Triton
  27. # kernel beside a cuBLAS bf16 GEMM; prefetch only forks a stream around the Engram lookup)
  28. "DSV41_DRAFT_HEAD_FP8", "DSV41_ENGRAM_PREFETCH", "DSV41_ENGRAM_PREFETCH_CHECK",
  29. # set by the launcher to a fresh timestamp on every boot: fingerprinting it made every
  30. # sidecar stale, so nothing was ever reused on dev-dsv41 (seen on TP3 2026-09-24)
  31. "SGLANG_RUN_ID", "DSV41_WO_A_W8_DRAFT")
  32. # Switches that change neither the tuned MoE shapes nor their kernels. On the TP4 launcher,
  33. # toggling one must reuse the tactics, or every A/B boot re-draws them (which moves the numerics).
  34. _VOLATILE_TP4 = ("DSV41_REPLICATED_SPLIT", "DSV41_REPLICATED_SPLIT_MAX_M", "DSV41_DRAFT_MAIN_PROJ_SPLIT",
  35. "DSV41_DRAFT_MAIN_PROJ_MAX_M", "DSV41_ROCE_GATHER", "DSV41_ROUTER_LIVE", "DSV41_DYN_SHARED",
  36. "DSV41_DYN_SHARED_RATIO", "DSV41_DYN_SHARED_FIXED", "DSV41_DYN_SHARED_MAX_M",
  37. "DSV41_DYN_SHARED_SERIAL", "DSV41_VERIFY_CAP_LOG", "DSV41_VERIFY_CAP_LOG_FEATURES",
  38. "DSV41_ROUTE_STATS", "DSV41_ROUTE_RING", "DSV41_DRAFT_TAU_POS", "DSV41_ENGRAM_DRM_NODE",
  39. "DSV41_ENGRAM_DRM_MIB", "DSV41_ENGRAM_DRM_LAYER", "DSV41_DRAFT_HEAD_FP8_IMPL")
  40. def _volatile():
  41. if os.environ.get("DSV41_LAUNCHER") == "tp4":
  42. return _VOLATILE + _VOLATILE_TP4
  43. return _VOLATILE
  44. def launch_fingerprint() -> str:
  45. volatile = _volatile()
  46. env = {k: v for k, v in os.environ.items() if k.startswith(_PREFIXES) and k not in volatile}
  47. payload = {"argv": sys.argv, "env": env}
  48. return hashlib.sha256(json.dumps(payload, sort_keys=True).encode()).hexdigest()
  49. def sidecar(cache_path: Path) -> Path:
  50. return cache_path.with_suffix(".launch")
  51. def install(mod):
  52. """sglang.srt.model_executor.runner.flashinfer_autotune"""
  53. if not ENABLED:
  54. return
  55. for name in ("_autotune_cache_digest", "flashinfer_autotune_context"):
  56. if not hasattr(mod, name):
  57. raise RuntimeError(f"DSV41_AUTOTUNE_KEEP: {name} is gone; engine drifted")
  58. if getattr(mod, "_dsv41_autotune_keep", False):
  59. return
  60. mod._dsv41_autotune_keep = True
  61. orig_ctx = mod.flashinfer_autotune_context
  62. fp = launch_fingerprint()
  63. # start-tp4.sh deletes a mismatched cache inside the digest. ./start.sh keeps the
  64. # previous behaviour: the digest only reports "", and the context drops the file.
  65. tp4 = os.environ.get("DSV41_LAUNCHER") == "tp4"
  66. def _autotune_cache_digest(cache_path, env):
  67. cache_path = Path(cache_path)
  68. if not cache_path.is_file():
  69. return ""
  70. try:
  71. configs = json.loads(cache_path.read_text())
  72. same_launch = sidecar(cache_path).read_text().strip() == fp
  73. except (OSError, ValueError):
  74. if not tp4:
  75. return ""
  76. configs, same_launch = None, False
  77. if not isinstance(configs, dict) or not same_launch:
  78. # a different launch re-tunes: an empty digest alone would still let the stock code
  79. # load the old file when every rank reports "" (they agree)
  80. if tp4:
  81. cache_path.unlink(missing_ok=True)
  82. return ""
  83. stamp = configs.get("_metadata")
  84. if not isinstance(stamp, dict):
  85. stamp = None
  86. # fp itself differs between ranks (node rank, per-node env); only "same launch as when
  87. # this rank saved" enters the cross-rank digest.
  88. payload = {"loadable": True, "stamp": stamp, "env": env}
  89. return hashlib.sha256(json.dumps(payload, sort_keys=True).encode()).hexdigest()
  90. import contextlib
  91. @contextlib.contextmanager
  92. def flashinfer_autotune_context(model_runner, *args, **kwargs):
  93. cache_path = Path(mod.flashinfer_autotune_cache_path(model_runner))
  94. before = _autotune_cache_digest(cache_path, {}) != ""
  95. if not tp4 and not before and cache_path.is_file():
  96. # No sidecar for this launch. When no rank has one, every digest is "" and the stock gate
  97. # sees agreement, so FlashInfer would still load each rank's stale file (the cache key
  98. # omits block size, graph sizes and adapter switches). Tune from scratch instead.
  99. cache_path.unlink(missing_ok=True)
  100. with orig_ctx(model_runner, *args, **kwargs):
  101. yield
  102. try:
  103. if cache_path.is_file():
  104. sidecar(cache_path).write_text(fp + "\n")
  105. except OSError:
  106. pass
  107. print(f"[autotune_keep] {'reused' if before else 'tuned and saved'} {cache_path.parent.name}/{cache_path.name}",
  108. flush=True)
  109. mod._autotune_cache_digest = _autotune_cache_digest
  110. mod.flashinfer_autotune_context = flashinfer_autotune_context
  111. print("[autotune_keep] armed", flush=True)