flashinfer-python 0.6.11.post3__py3-none-any.whl → 0.6.12__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (170) hide show
  1. build_backend.py +704 -0
  2. flashinfer/_build_meta.py +2 -2
  3. flashinfer/aot.py +5 -68
  4. flashinfer/api_logging.py +876 -62
  5. flashinfer/artifacts.py +6 -6
  6. flashinfer/autotuner.py +79 -48
  7. flashinfer/comm/allreduce.py +12 -0
  8. flashinfer/comm/trtllm_ar.py +22 -0
  9. flashinfer/comm/trtllm_mnnvl_ar.py +7 -0
  10. flashinfer/cute_dsl/attention/__init__.py +8 -1
  11. flashinfer/cute_dsl/attention/fusion/variant.py +81 -0
  12. flashinfer/cute_dsl/attention/mla_config.py +2 -4
  13. flashinfer/cute_dsl/attention/mla_decode.py +13 -9
  14. flashinfer/cute_dsl/attention/mla_decode_fp8.py +13 -9
  15. flashinfer/cute_dsl/attention/mla_dispatch.py +126 -0
  16. flashinfer/cute_dsl/attention/monolithic/__init__.py +41 -0
  17. flashinfer/cute_dsl/attention/monolithic/mla_decode.py +490 -0
  18. flashinfer/cute_dsl/attention/monolithic/mla_decode_fp16.py +3529 -0
  19. flashinfer/cute_dsl/attention/monolithic/mla_decode_fp8.py +3501 -0
  20. flashinfer/cute_dsl/attention/monolithic/mla_helpers.py +304 -0
  21. flashinfer/cute_dsl/attention/roles/mma.py +2 -2
  22. flashinfer/cute_dsl/attention/scheduler/mla_persistent.py +4 -1
  23. flashinfer/cute_dsl/attention/wrappers/batch_mla.py +58 -22
  24. flashinfer/cute_dsl/attention/wrappers/batch_prefill.py +4 -1
  25. flashinfer/cute_dsl/gemm_allreduce_two_shot.py +2 -2
  26. flashinfer/data/build_backend.py +704 -0
  27. flashinfer/data/csrc/api_log_stats.cu +196 -0
  28. flashinfer/data/csrc/checkpointing_ssu.cu +546 -0
  29. flashinfer/data/csrc/checkpointing_ssu_customize_config.jinja +38 -0
  30. flashinfer/data/csrc/checkpointing_ssu_jit_binding.cu +53 -0
  31. flashinfer/data/csrc/checkpointing_ssu_kernel_inst.cu +12 -0
  32. flashinfer/data/csrc/cutlass_mla.cu +52 -12
  33. flashinfer/data/csrc/dsv3_router_gemm.cu +13 -6
  34. flashinfer/data/csrc/flashinfer_api_log_stats_binding.cu +20 -0
  35. flashinfer/data/csrc/flashinfer_mla_binding.cu +1 -1
  36. flashinfer/data/csrc/flashinfer_norm_binding.cu +11 -0
  37. flashinfer/data/csrc/fmhaReduction.cu +102 -53
  38. flashinfer/data/csrc/fmha_cutlass_sm100.cu +3 -1
  39. flashinfer/data/csrc/fmha_v2/fmha/warpspec/circular_buffer.h +19 -11
  40. flashinfer/data/csrc/fmha_v2/fmha/warpspec/dma.h +67 -18
  41. flashinfer/data/csrc/fmha_v2/fmha/warpspec/kernel_traits.h +3 -2
  42. flashinfer/data/csrc/fused_moe/cutlass_backend/cutlass_fused_moe_kernels.cuh +131 -41
  43. flashinfer/data/csrc/moe_utils_binding.cu +53 -0
  44. flashinfer/data/csrc/norm.cu +39 -0
  45. flashinfer/data/csrc/nv_internal/cpp/common/envUtils.cpp +30 -1
  46. flashinfer/data/csrc/nv_internal/cpp/kernels/quantization.cu +267 -180
  47. flashinfer/data/csrc/nv_internal/include/tensorrt_llm/common/quantization.h +23 -0
  48. flashinfer/data/csrc/nv_internal/tensorrt_llm/common/envUtils.h +17 -0
  49. flashinfer/data/csrc/nv_internal/tensorrt_llm/kernels/cutlass_kernels/moe_gemm/launchers/moe_gemm_tma_ws_launcher.inl +2 -1
  50. flashinfer/data/csrc/nv_internal/tensorrt_llm/kernels/quantization.cuh +142 -73
  51. flashinfer/data/csrc/nv_internal/tensorrt_llm/kernels/quantization.h +1 -1
  52. flashinfer/data/csrc/nv_internal/tensorrt_llm/kernels/quantization_utils.cuh +294 -36
  53. flashinfer/data/csrc/trtllm_allreduce_fusion.cu +3 -1
  54. flashinfer/data/csrc/trtllm_batched_gemm_runner.cu +10 -2
  55. flashinfer/data/csrc/trtllm_fmha_kernel_launcher.cu +248 -36
  56. flashinfer/data/csrc/trtllm_fused_moe_kernel_launcher.cu +80 -42
  57. flashinfer/data/csrc/trtllm_fused_moe_runner.cu +23 -11
  58. flashinfer/data/csrc/trtllm_gemm_runner.cu +8 -8
  59. flashinfer/data/csrc/trtllm_low_latency_gemm_runner.cu +8 -8
  60. flashinfer/data/csrc/trtllm_mnnvl_allreduce.cu +2 -1
  61. flashinfer/data/csrc/trtllm_moe_allreduce_fusion.cu +6 -2
  62. flashinfer/data/csrc/xqa/mha.cu +13 -7
  63. flashinfer/data/include/flashinfer/attention/blackwell/fmha_cutlass_sm100.cuh +3 -0
  64. flashinfer/data/include/flashinfer/attention/cutlass_mla.cuh +9 -8
  65. flashinfer/data/include/flashinfer/comm/trtllm_allreduce_fusion.cuh +6 -3
  66. flashinfer/data/include/flashinfer/comm/trtllm_mnnvl_allreduce.cuh +31 -17
  67. flashinfer/data/include/flashinfer/comm/trtllm_moe_allreduce_fusion.cuh +9 -3
  68. flashinfer/data/include/flashinfer/gemm/dsv3_router_gemm.cuh +2 -2
  69. flashinfer/data/include/flashinfer/mamba/checkpointing_ssu.cuh +154 -0
  70. flashinfer/data/include/flashinfer/mamba/common.cuh +3 -0
  71. flashinfer/data/include/flashinfer/mamba/conversion.cuh +265 -8
  72. flashinfer/data/include/flashinfer/mamba/invoke_selective_state_update_mtp.cuh +90 -125
  73. flashinfer/data/include/flashinfer/mamba/kernel_checkpointing_ssu.cuh +1131 -0
  74. flashinfer/data/include/flashinfer/mamba/kernel_checkpointing_ssu_8bit.cuh +1495 -0
  75. flashinfer/data/include/flashinfer/mamba/kernel_checkpointing_ssu_common.cuh +1671 -0
  76. flashinfer/data/include/flashinfer/mamba/kernel_selective_state_update_mtp_horizontal.cuh +44 -84
  77. flashinfer/data/include/flashinfer/mamba/kernel_selective_state_update_mtp_simple.cuh +578 -465
  78. flashinfer/data/include/flashinfer/mamba/kernel_selective_state_update_mtp_vertical.cuh +30 -26
  79. flashinfer/data/include/flashinfer/mamba/launch_checkpointing_ssu.cuh +181 -0
  80. flashinfer/data/include/flashinfer/mamba/ssu_mtp_common.cuh +56 -0
  81. flashinfer/data/include/flashinfer/norm/fused_qk_rmsnorm_rope.cuh +773 -0
  82. flashinfer/data/include/flashinfer/trtllm/fmha/fmhaKernels.cuh +96 -35
  83. flashinfer/data/include/flashinfer/trtllm/fmha/fmhaRunnerParams.h +33 -3
  84. flashinfer/data/include/flashinfer/trtllm/fmha/kernelParams.h +39 -3
  85. flashinfer/data/include/flashinfer/trtllm/fmha/lse.cuh +45 -11
  86. flashinfer/data/include/flashinfer/trtllm/fused_moe/runner.h +7 -6
  87. flashinfer/decode.py +63 -3
  88. flashinfer/diffusion_ops/__init__.py +2 -0
  89. flashinfer/fused_moe/core.py +88 -31
  90. flashinfer/fused_moe/cute_dsl/_inputs_helper.py +198 -0
  91. flashinfer/fused_moe/cute_dsl/blackwell/blockscaled_contiguous_gather_grouped_gemm_swiglu_fusion.py +2 -2
  92. flashinfer/fused_moe/cute_dsl/blackwell/blockscaled_contiguous_grouped_gemm_finalize_fusion.py +2 -2
  93. flashinfer/fused_moe/cute_dsl/blackwell_sm12x/moe_dynamic_kernel.py +16 -12
  94. flashinfer/fused_moe/cute_dsl/blackwell_sm12x/moe_w4a16_host.py +2 -2
  95. flashinfer/fused_moe/cute_dsl/blackwell_sm12x/moe_w4a16_kernel.py +5 -4
  96. flashinfer/fused_moe/cute_dsl/fused_moe.py +93 -23
  97. flashinfer/fused_moe/cute_dsl/moe_utils.py +60 -0
  98. flashinfer/fused_moe/cute_dsl/tuner.py +56 -8
  99. flashinfer/gdn_decode.py +20 -11
  100. flashinfer/gdn_kernels/blackwell/gated_delta_net_chunked.py +1 -1
  101. flashinfer/gdn_kernels/gdn_decode_bf16_state.py +133 -102
  102. flashinfer/gdn_kernels/gdn_decode_pretranspose.py +21 -6
  103. flashinfer/gdn_prefill.py +10 -2
  104. flashinfer/gemm/__init__.py +2 -20
  105. flashinfer/gemm/gemm_base.py +308 -377
  106. flashinfer/gemm/kernels/dense_blockscaled_gemm_sm100.py +2 -2
  107. flashinfer/gemm/kernels/dense_blockscaled_gemm_sm103.py +2 -2
  108. flashinfer/gemm/kernels/dense_blockscaled_gemm_sm120_b12x.py +2 -2
  109. flashinfer/gemm/kernels/grouped_gemm_masked_blackwell.py +678 -73
  110. flashinfer/gemm/routergemm.py +94 -11
  111. flashinfer/jit/api_log_stats.py +28 -0
  112. flashinfer/jit/attention/fmha_v2/fmha_library.py +15 -2
  113. flashinfer/jit/core.py +1 -0
  114. flashinfer/jit/mamba/checkpointing_ssu.py +188 -0
  115. flashinfer/jit/mamba/selective_state_update.py +8 -0
  116. flashinfer/jit/moe_utils.py +6 -1
  117. flashinfer/mamba/__init__.py +2 -1
  118. flashinfer/mamba/checkpointing_ssu.py +468 -0
  119. flashinfer/mamba/selective_state_update.py +7 -1
  120. flashinfer/mamba/ssd_kernel.py +2 -2
  121. flashinfer/mla/_core.py +543 -7
  122. flashinfer/moe_ep/__init__.py +128 -0
  123. flashinfer/moe_ep/nccl_ep/__init__.py +118 -0
  124. flashinfer/moe_ep/nixl_ep/__init__.py +168 -0
  125. flashinfer/norm/__init__.py +312 -2
  126. flashinfer/prefill.py +57 -18
  127. flashinfer/quantization/fp4_quantization.py +66 -7
  128. flashinfer/quantization/fp8_quantization.py +4 -4
  129. flashinfer/quantization/kernels/mxfp4_quantize.py +36 -18
  130. flashinfer/quantization/kernels/mxfp8_quantize.py +46 -16
  131. flashinfer/trace/template.py +125 -6
  132. flashinfer/trace/templates/_init_helpers.py +270 -0
  133. flashinfer/trace/templates/activation.py +91 -9
  134. flashinfer/trace/templates/attention.py +746 -5
  135. flashinfer/trace/templates/cascade.py +68 -0
  136. flashinfer/trace/templates/comm.py +81 -2
  137. flashinfer/trace/templates/gdn.py +193 -0
  138. flashinfer/trace/templates/gemm.py +644 -8
  139. flashinfer/trace/templates/mamba.py +39 -0
  140. flashinfer/trace/templates/moe.py +401 -2
  141. flashinfer/trace/templates/norm.py +276 -0
  142. flashinfer/trace/templates/page.py +430 -17
  143. flashinfer/trace/templates/quantize.py +136 -1
  144. flashinfer/trace/templates/rope.py +393 -15
  145. flashinfer/trace/templates/sampling.py +319 -2
  146. flashinfer/utils.py +2 -8
  147. {flashinfer_python-0.6.11.post3.dist-info → flashinfer_python-0.6.12.dist-info}/METADATA +4 -2
  148. {flashinfer_python-0.6.11.post3.dist-info → flashinfer_python-0.6.12.dist-info}/RECORD +152 -144
  149. {flashinfer_python-0.6.11.post3.dist-info → flashinfer_python-0.6.12.dist-info}/WHEEL +1 -1
  150. flashinfer/env.py +0 -28
  151. flashinfer/gemm/specialized_kernels/__init__.py +0 -21
  152. flashinfer/gemm/specialized_kernels/_utils.py +0 -12
  153. flashinfer/gemm/specialized_kernels/bmm_fp8_sm121/__init__.py +0 -11
  154. flashinfer/gemm/specialized_kernels/bmm_fp8_sm121/bmm_fp8_sm121.py +0 -265
  155. flashinfer/gemm/specialized_kernels/bmm_fp8_sm121/cuda/__init__.py +0 -1
  156. flashinfer/gemm/specialized_kernels/bmm_fp8_sm121/cuda/binding.cu +0 -143
  157. flashinfer/gemm/specialized_kernels/bmm_fp8_sm121/cuda/kernel.cu +0 -972
  158. flashinfer/gemm/specialized_kernels/bmm_fp8_sm121/cute_dsl/__init__.py +0 -1
  159. flashinfer/gemm/specialized_kernels/bmm_fp8_sm121/cute_dsl/kernel.py +0 -1920
  160. flashinfer/gemm/specialized_kernels/bmm_fp8_sm121/cutile/__init__.py +0 -1
  161. flashinfer/gemm/specialized_kernels/bmm_fp8_sm121/cutile/kernel.py +0 -328
  162. flashinfer/gemm/specialized_kernels/bmm_fp8_sm121/workloads.json +0 -191
  163. flashinfer/gemm/specialized_kernels/mm_fp4_sm121/__init__.py +0 -11
  164. flashinfer/gemm/specialized_kernels/mm_fp4_sm121/cute_dsl/__init__.py +0 -1
  165. flashinfer/gemm/specialized_kernels/mm_fp4_sm121/cute_dsl/kernel.py +0 -2284
  166. flashinfer/gemm/specialized_kernels/mm_fp4_sm121/mm_fp4_sm121.py +0 -244
  167. flashinfer/gemm/specialized_kernels/mm_fp4_sm121/workloads.json +0 -1091
  168. {flashinfer_python-0.6.11.post3.dist-info → flashinfer_python-0.6.12.dist-info}/entry_points.txt +0 -0
  169. {flashinfer_python-0.6.11.post3.dist-info → flashinfer_python-0.6.12.dist-info}/licenses/LICENSE +0 -0
  170. {flashinfer_python-0.6.11.post3.dist-info → flashinfer_python-0.6.12.dist-info}/top_level.txt +0 -0
build_backend.py CHANGED
@@ -16,6 +16,10 @@ limitations under the License.
16
16
 
17
17
  import os
18
18
  import shutil
19
+ import subprocess
20
+ import sys
21
+ import time
22
+ from contextlib import contextmanager
19
23
  from pathlib import Path
20
24
 
21
25
  from setuptools import build_meta as orig
@@ -25,6 +29,702 @@ _root = Path(__file__).parent.resolve()
25
29
  _data_dir = _root / "flashinfer" / "data"
26
30
 
27
31
 
32
+ # moe_ep build infra. Three opt-in switches, all `0` by default:
33
+ # BUILD_NCCL_EP=1 → build NCCL-EP from 3rdparty/nccl
34
+ # BUILD_NIXL_EP=1 → build NIXL-EP from 3rdparty/nixl
35
+ # BUILD_NVEP=1 → legacy alias: turns BOTH on (back-compat with earlier docs)
36
+ #
37
+ # Each backend has independent system-dep requirements (NIXL needs DOCA
38
+ # gpunetio + UCX 1.21.x; NCCL doesn't). Hosts that only have one backend's
39
+ # deps should opt in with the matching flag instead of BUILD_NVEP, so a
40
+ # failing build on the missing backend doesn't abort the whole install.
41
+ def _flag(name: str) -> bool:
42
+ v = os.environ.get(name, "")
43
+ return v == "1" or v.lower() in ("true", "yes", "on")
44
+
45
+
46
+ @contextmanager
47
+ def _time_phase(label: str):
48
+ """Emit a wall-clock duration line for a build phase.
49
+
50
+ Docker buffers the entire `RUN` layer's output, so without these markers
51
+ you can't tell from a finished build log how long each backend took. The
52
+ lines are flushed explicitly so they survive a SIGKILL on the parent.
53
+ """
54
+ print(f"[BUILD_NVEP] {label}: start", flush=True)
55
+ t0 = time.monotonic()
56
+ try:
57
+ yield
58
+ finally:
59
+ dt = time.monotonic() - t0
60
+ print(f"[BUILD_NVEP] {label}: done in {dt:.1f}s", flush=True)
61
+
62
+
63
+ _BUILD_NVEP = _flag("BUILD_NVEP")
64
+ _BUILD_NCCL_EP = _flag("BUILD_NCCL_EP") or _BUILD_NVEP
65
+ _BUILD_NIXL_EP = _flag("BUILD_NIXL_EP") or _BUILD_NVEP
66
+
67
+ # Was the user opting in via the legacy "give me everything possible" alias
68
+ # (BUILD_NVEP=1) AND NOT explicitly via the per-backend switches? If yes,
69
+ # treat a missing build-time dep as "skip that backend with a warning"
70
+ # instead of "abort the entire install". When the user explicitly asks for
71
+ # BUILD_NCCL_EP=1 / BUILD_NIXL_EP=1, a missing dep is a hard error — they
72
+ # asked for that backend specifically.
73
+ _BUILD_NVEP_BEST_EFFORT = _BUILD_NVEP and not (
74
+ _flag("BUILD_NCCL_EP") or _flag("BUILD_NIXL_EP")
75
+ )
76
+
77
+ _nvep_build_root = _root / "build_nvep"
78
+ _moe_ep_pkg = _root / "flashinfer" / "moe_ep"
79
+
80
+
81
+ def _detect_cuda_major() -> int:
82
+ """Best-effort detection of the CUDA major version on the host."""
83
+ try:
84
+ out = subprocess.check_output(["nvcc", "--version"]).decode()
85
+ for line in out.splitlines():
86
+ if "release" in line:
87
+ # e.g. "Cuda compilation tools, release 13.0, V13.0.48"
88
+ token = line.split("release", 1)[1].split(",", 1)[0].strip()
89
+ return int(token.split(".")[0])
90
+ except Exception:
91
+ pass
92
+ return 13 # default — pyproject's nvep extras pin cu13 packages
93
+
94
+
95
+ def _apply_patches(submodule_dir: Path, patches_dir: Path) -> None:
96
+ """Apply every *.patch in patches_dir to the submodule working tree.
97
+
98
+ No-ops if patches_dir doesn't exist. Idempotent: skips patches that are
99
+ already applied by checking `git apply --reverse --check` first.
100
+ """
101
+ if not patches_dir.is_dir():
102
+ return
103
+ for patch in sorted(patches_dir.glob("*.patch")):
104
+ # Already applied?
105
+ already = subprocess.run(
106
+ ["git", "apply", "--reverse", "--check", str(patch)],
107
+ cwd=submodule_dir,
108
+ capture_output=True,
109
+ )
110
+ if already.returncode == 0:
111
+ print(f"[BUILD_NVEP] patch already applied, skipping: {patch.name}")
112
+ continue
113
+ # Check we *can* apply, then apply.
114
+ subprocess.run(
115
+ ["git", "apply", "--check", str(patch)],
116
+ cwd=submodule_dir,
117
+ check=True,
118
+ )
119
+ subprocess.run(
120
+ ["git", "apply", str(patch)],
121
+ cwd=submodule_dir,
122
+ check=True,
123
+ )
124
+ print(f"[BUILD_NVEP] applied patch: {patch.name}")
125
+
126
+
127
+ def _find_nixl_wheel_lib_dir() -> Path | None:
128
+ """Locate the nixl-cu* pip wheel's libnixl.so directory.
129
+
130
+ Layout of the current nixl-cu13 wheel (meson-python packaging):
131
+ <site-packages>/nixl_cu13/ — importable python module
132
+ <site-packages>/.nixl_cu13.mesonpy.libs/ — libnixl.so + sibling libs
133
+ <site-packages>/nixl_cu13.libs/ — auditwheel deps + plugins
134
+
135
+ We probe by importing `nixl_cu13` / `nixl_cu12` / `nixl` (legacy), then
136
+ look for `.{name}.mesonpy.libs/libnixl.so` next to it.
137
+
138
+ `<machine>-linux-gnu` follows the Debian multiarch convention used by
139
+ auditwheel-tagged wheels — `x86_64-linux-gnu` on x86_64, and
140
+ `aarch64-linux-gnu` on ARM64 (e.g. NVIDIA Grace / AWS Graviton hosts).
141
+ """
142
+ import platform
143
+
144
+ multiarch = f"{platform.machine()}-linux-gnu"
145
+ for pkg_name in ("nixl_cu13", "nixl_cu12", "nixl"):
146
+ try:
147
+ mod = __import__(pkg_name)
148
+ except ImportError:
149
+ continue
150
+ try:
151
+ pkg_root = Path(mod.__path__[0])
152
+ except Exception:
153
+ continue
154
+ site_packages = pkg_root.parent
155
+ # Prefer the meson-python sibling layout.
156
+ for candidate in (
157
+ site_packages / f".{pkg_name}.mesonpy.libs",
158
+ pkg_root / "lib" / multiarch,
159
+ pkg_root / "lib",
160
+ pkg_root,
161
+ ):
162
+ if (candidate / "libnixl.so").exists():
163
+ return candidate
164
+ # Last resort: a glob-walk of site-packages for any .nixl_*.mesonpy.libs/.
165
+ try:
166
+ sp = Path(__import__("site").getsitepackages()[0]) # type: ignore[no-untyped-call]
167
+ except Exception:
168
+ return None
169
+ for candidate in sp.glob(".nixl*.mesonpy.libs"):
170
+ if (candidate / "libnixl.so").exists():
171
+ return candidate
172
+ return None
173
+
174
+
175
+ def _build_nixl_ep() -> None:
176
+ src = _root / "3rdparty" / "nixl"
177
+ build = _nvep_build_root / "nixl"
178
+ prefix = _nvep_build_root / "nixl_install"
179
+ _apply_patches(src, _root / "3rdparty_patches" / "nixl")
180
+
181
+ # Default path: skip the parent libnixl build and link the EP example
182
+ # against the libnixl.so shipped by the nixl-cu13 pip wheel — mirrors the
183
+ # contrib/nccl_ep wheel-driven path (Section 9 of the integration plan).
184
+ # The hermetic env var falls back to the full parent build for hosts
185
+ # without the wheel pre-installed.
186
+ hermetic = _flag("BUILD_NIXL_EP_HERMETIC")
187
+ setup_args = [
188
+ "meson",
189
+ "setup",
190
+ str(build),
191
+ str(src),
192
+ "-Dbuild_nixl_ep=true",
193
+ f"-Dprefix={prefix}",
194
+ "--buildtype=release",
195
+ ]
196
+ if hermetic:
197
+ print("[BUILD_NVEP] BUILD_NIXL_EP_HERMETIC=1 — building full NIXL tree")
198
+ setup_args.append("-Dbuild_examples=true")
199
+ else:
200
+ wheel_lib_dir = _find_nixl_wheel_lib_dir()
201
+ if wheel_lib_dir is None:
202
+ raise RuntimeError(
203
+ "BUILD_NIXL_EP requires nixl-cu13 to be pre-installed.\n"
204
+ "Run: uv pip install --no-deps 'nixl-cu13>=1.0.1'\n"
205
+ "(the FlashInfer Dockerfile does this automatically; bare-host\n"
206
+ "installs need to do it before `pip install -e .[nvep]`).\n"
207
+ "Or set BUILD_NIXL_EP_HERMETIC=1 to build the full NIXL tree."
208
+ )
209
+ setup_args += [
210
+ "-Dbuild_examples=false",
211
+ "-Dnixl_ep_only=true",
212
+ f"-Dnixl_wheel_lib_dir={wheel_lib_dir}",
213
+ ]
214
+ print(
215
+ f"[BUILD_NVEP] nixl_ep_only=true; linking against wheel libnixl.so "
216
+ f"at {wheel_lib_dir}. Set BUILD_NIXL_EP_HERMETIC=1 to opt out."
217
+ )
218
+
219
+ # Re-run meson setup with --reconfigure when the build dir already
220
+ # exists, so patch / option changes (e.g. a different wheel path,
221
+ # flipping HERMETIC mode) take effect on subsequent installs without
222
+ # requiring the user to `rm -rf build_nvep/nixl` manually.
223
+ if build.exists():
224
+ setup_args.append("--reconfigure")
225
+ subprocess.run(setup_args, check=True)
226
+
227
+ # `install` only makes sense in hermetic mode (it populates `prefix/` with
228
+ # libnixl + headers + plugins). In ep_only mode there's nothing to install
229
+ # — we just compile and pluck nixl_ep_cpp.so out of the build tree.
230
+ ninja_cmd = ["ninja", "-C", str(build)]
231
+ if hermetic:
232
+ ninja_cmd.append("install")
233
+ subprocess.run(ninja_cmd, check=True)
234
+
235
+ dst = _moe_ep_pkg / "nixl_ep" / "_libs"
236
+ dst.mkdir(parents=True, exist_ok=True)
237
+
238
+ # We do NOT stage the base NIXL libraries (libnixl.so, libnixl_capi.so,
239
+ # libserdes.so, etc.) — they come from the `nixl-cu13` pip wheel installed
240
+ # by _install_nvep_runtime_wheels(). The runtime loader in
241
+ # flashinfer/moe_ep/nixl_ep/__init__.py ctypes-preloads them via the wheel's
242
+ # site-packages path before loading nixl_ep_cpp.so. This keeps the
243
+ # FlashInfer wheel small.
244
+
245
+ # The torch extension lands either in build/ or build/examples/device/ep/
246
+ for cand in (build / "examples/device/ep").glob("nixl_ep_cpp*.so"):
247
+ shutil.copy(cand, dst / cand.name)
248
+ print(f"[BUILD_NVEP] staged: {cand.name}")
249
+
250
+ # Vendor the python wrapper sources so we can import from
251
+ # flashinfer.moe_ep.nixl_ep._vendored (Step B5).
252
+ vendored_src = src / "examples/device/ep/nixl_ep"
253
+ if vendored_src.exists():
254
+ shutil.copytree(
255
+ vendored_src,
256
+ _moe_ep_pkg / "nixl_ep" / "_vendored",
257
+ dirs_exist_ok=True,
258
+ )
259
+
260
+
261
+ def _find_nccl_wheel_root() -> Path | None:
262
+ """Locate the nvidia-nccl-cu13 pip wheel's nvidia/nccl/ directory.
263
+
264
+ Returns the resolved path or None if the wheel isn't installed in the
265
+ Python environment used by this build hook.
266
+ """
267
+ try:
268
+ import nvidia.nccl # type: ignore[import-not-found]
269
+ except ImportError:
270
+ return None
271
+ try:
272
+ return Path(nvidia.nccl.__path__[0])
273
+ except Exception:
274
+ return None
275
+
276
+
277
+ def _synthesize_nccl_builddir(build: Path) -> None:
278
+ """Symlink the pip wheel's NCCL include + lib into a fake BUILDDIR.
279
+
280
+ contrib/nccl_ep's Makefile references $(BUILDDIR)/include and
281
+ $(BUILDDIR)/lib/libnccl.so. These were historically populated by
282
+ `make src.build` (~10 min, building libnccl.so.2 from source). The
283
+ nvidia-nccl-cu13 pip wheel ships the exact same public headers
284
+ (verified against the submodule's src/include/nccl_device.h via
285
+ `diff -q`) and an ABI-compatible libnccl.so.2, so we can just point
286
+ BUILDDIR at it and skip the source build entirely.
287
+
288
+ Header/SHA drift between the wheel and our submodule pin is checked
289
+ via NCCL_VERSION_CODE comparison; a mismatch warns but does not
290
+ hard-fail (user might be intentionally pinning a different version).
291
+ """
292
+ wheel = _find_nccl_wheel_root()
293
+ if wheel is None:
294
+ raise RuntimeError(
295
+ "BUILD_NCCL_EP requires nvidia-nccl-cu13 to be pre-installed.\n"
296
+ "Run: uv pip install --no-deps 'nvidia-nccl-cu13>=2.30.4'\n"
297
+ "(the FlashInfer Dockerfile does this automatically; bare-host\n"
298
+ "installs need to do it before `pip install -e .[nvep]`)."
299
+ )
300
+ build.mkdir(parents=True, exist_ok=True)
301
+
302
+ # include/ — symlink the entire dir from the wheel
303
+ inc_target = build / "include"
304
+ if inc_target.is_symlink() or inc_target.exists():
305
+ if inc_target.is_symlink() or inc_target.is_file():
306
+ inc_target.unlink()
307
+ else:
308
+ shutil.rmtree(inc_target)
309
+ inc_target.symlink_to(wheel / "include", target_is_directory=True)
310
+
311
+ # lib/ — symlink libnccl.so and libnccl.so.2 to the wheel's libnccl.so.2.
312
+ # Two names because contrib/nccl_ep's Makefile uses -lnccl (which resolves
313
+ # via libnccl.so SONAME), and the linker may also reference libnccl.so.2
314
+ # for SONAME resolution.
315
+ lib_dir = build / "lib"
316
+ lib_dir.mkdir(exist_ok=True)
317
+ libnccl = wheel / "lib" / "libnccl.so.2"
318
+ if not libnccl.exists():
319
+ raise RuntimeError(
320
+ f"Found nvidia-nccl-cu13 wheel at {wheel} but its lib/libnccl.so.2 "
321
+ "is missing. Reinstall the wheel."
322
+ )
323
+ for soname in ("libnccl.so", "libnccl.so.2"):
324
+ link = lib_dir / soname
325
+ if link.is_symlink() or link.exists():
326
+ link.unlink()
327
+ link.symlink_to(libnccl)
328
+
329
+ # SHA/version sanity check between submodule and wheel — warn only.
330
+ _check_nccl_version_drift(wheel)
331
+
332
+ print(f"[BUILD_NVEP] synthesized BUILDDIR={build} from wheel at {wheel}")
333
+
334
+
335
+ def _check_nccl_version_drift(wheel: Path) -> None:
336
+ """Compare NCCL_VERSION_CODE between the wheel's nccl.h and our submodule.
337
+
338
+ The submodule's nccl.h.in has e.g. `NCCL_VERSION_CODE = 23004` (NCCL 2.30.4).
339
+ If the wheel's installed nccl.h has a different code, warn — we'll build
340
+ against the wheel's ABI which may differ from the submodule we patched.
341
+ """
342
+ import re
343
+
344
+ src = _root / "3rdparty" / "nccl" / "src" / "nccl.h.in"
345
+ wheel_h = wheel / "include" / "nccl.h"
346
+
347
+ def _version(path: Path) -> int | None:
348
+ try:
349
+ text = path.read_text()
350
+ except Exception:
351
+ return None
352
+ m = re.search(r"NCCL_VERSION_CODE\s+(\d+)", text)
353
+ return int(m.group(1)) if m else None
354
+
355
+ sub_v, whl_v = _version(src), _version(wheel_h)
356
+ if sub_v is None or whl_v is None:
357
+ return # can't parse; silently skip
358
+ if sub_v != whl_v:
359
+ print(
360
+ f"[BUILD_NVEP] WARNING: NCCL_VERSION_CODE drift — "
361
+ f"submodule={sub_v}, wheel={whl_v}. "
362
+ "Building contrib/nccl_ep against the wheel's ABI. If you need "
363
+ "the submodule's ABI, set BUILD_NCCL_EP_HERMETIC=1 to fall back "
364
+ "to `make src.build`."
365
+ )
366
+
367
+
368
+ def _build_nccl_ep() -> None:
369
+ src = _root / "3rdparty" / "nccl"
370
+ build = _nvep_build_root / "nccl"
371
+ _apply_patches(src, _root / "3rdparty_patches" / "nccl")
372
+
373
+ # Skip the heavy `make src.build` (~10 min) by pointing BUILDDIR at the
374
+ # pip-installed nvidia-nccl-cu13 wheel. The opt-out env var falls back
375
+ # to the from-source build for users who can't pre-install the wheel.
376
+ if _flag("BUILD_NCCL_EP_HERMETIC"):
377
+ print("[BUILD_NVEP] BUILD_NCCL_EP_HERMETIC=1 — building libnccl from source")
378
+ subprocess.run(
379
+ ["make", "src.build", f"BUILDDIR={build}", "-j"],
380
+ cwd=src,
381
+ check=True,
382
+ )
383
+ else:
384
+ _synthesize_nccl_builddir(build)
385
+
386
+ # contrib/nccl_ep's Makefile refuses any gencode below sm_90 (see
387
+ # 3rdparty/nccl/contrib/nccl_ep/Makefile:15). Override NVCC_GENCODE to only
388
+ # cover the EP-supported arches: sm_90 (H100), sm_100 (B200), sm_103 (B300).
389
+ nccl_ep_gencode = " ".join(
390
+ [
391
+ "-gencode=arch=compute_90,code=sm_90",
392
+ "-gencode=arch=compute_100,code=sm_100",
393
+ "-gencode=arch=compute_103,code=sm_103",
394
+ ]
395
+ )
396
+ subprocess.run(
397
+ [
398
+ "make",
399
+ "-C",
400
+ "contrib/nccl_ep",
401
+ f"BUILDDIR={build}",
402
+ f"NVCC_GENCODE={nccl_ep_gencode}",
403
+ "-j",
404
+ ],
405
+ cwd=src,
406
+ check=True,
407
+ )
408
+
409
+ dst = _moe_ep_pkg / "nccl_ep" / "_libs"
410
+ dst.mkdir(parents=True, exist_ok=True)
411
+ # We do NOT stage libnccl.so.2 — it comes from the `nvidia-nccl-cu13`
412
+ # pip wheel installed by _install_nvep_runtime_wheels(). The runtime
413
+ # loader in flashinfer/moe_ep/nccl_ep/__init__.py ctypes-preloads it
414
+ # via the wheel's site-packages path before loading libnccl_ep.so.
415
+ # This keeps the FlashInfer wheel ~200 MB smaller.
416
+ for soname in ("libnccl_ep.so",):
417
+ sopath = build / "lib" / soname
418
+ if sopath.exists():
419
+ shutil.copy(sopath, dst / soname)
420
+ print(f"[BUILD_NVEP] staged: {soname}")
421
+
422
+ # NOTE: nccl_ep (ctypes wrapper from contrib/nccl_ep/python) and nccl4py
423
+ # (Cython bindings + Communicator(ptr=...) bridge) are NOT pip-installed
424
+ # from this build hook. When `uv pip install` runs the FlashInfer build,
425
+ # sys.executable points to uv's isolated build env (which has no pip), so
426
+ # `python -m pip install` from here fails. Install them as a separate
427
+ # post-build step against the target venv:
428
+ #
429
+ # pip install -e 3rdparty/nccl/contrib/nccl_ep/python
430
+ # CUDA_HOME=/usr/local/cuda pip install -e 3rdparty/nccl/bindings/nccl4py[cu13]
431
+ #
432
+ # docker/Dockerfile.flashinfer-nvep already chains these after the main
433
+ # `BUILD_NVEP=1 uv pip install ...` step.
434
+ print(
435
+ "[BUILD_NVEP] nccl_ep + nccl4py pip-installs deferred to post-build "
436
+ "step (see docker/Dockerfile.flashinfer-nvep). Skipping in hook."
437
+ )
438
+
439
+
440
+ def _fix_rpaths() -> None:
441
+ """Rewrite RPATHs on staged .so files so they find siblings without LD_LIBRARY_PATH.
442
+
443
+ Since we no longer stage the base libs (libnccl.so.2, libnixl.so) inside
444
+ the package, the RPATH only needs to cover $ORIGIN and $ORIGIN/_libs for
445
+ co-located plugin files. The base libs are loaded explicitly at Python
446
+ import time via the runtime preloaders in
447
+ flashinfer/moe_ep/{nccl,nixl}_ep/__init__.py.
448
+ """
449
+ patchelf_ok = shutil.which("patchelf") is not None
450
+ if not patchelf_ok:
451
+ print("[BUILD_NVEP] patchelf not found; skipping RPATH fix-up")
452
+ return
453
+ rpath = "$ORIGIN:$ORIGIN/_libs"
454
+ for so in _moe_ep_pkg.rglob("*.so*"):
455
+ # Skip symlinks
456
+ if so.is_symlink():
457
+ continue
458
+ # Use check=False because patchelf legitimately exits nonzero on
459
+ # files that already have the desired RPATH or that aren't ELFs
460
+ # we care about. Surface anything else as a warning so a real
461
+ # failure (e.g. binary lacks .dynamic section) isn't silently lost.
462
+ r = subprocess.run(
463
+ ["patchelf", "--set-rpath", rpath, str(so)],
464
+ capture_output=True,
465
+ text=True,
466
+ )
467
+ if r.returncode != 0:
468
+ err = (r.stderr or r.stdout or "").strip()
469
+ print(f"[BUILD_NVEP] WARNING: patchelf failed on {so.name}: {err}")
470
+
471
+
472
+ def _nixl_buildable() -> tuple[bool, str]:
473
+ """Probe for hard NIXL-EP build-time deps. Returns (ok, reason_if_not).
474
+
475
+ In the default (wheel-driven) flow, the EP example links against the
476
+ nixl-cu13 pip wheel — so it must be importable. In hermetic mode
477
+ (BUILD_NIXL_EP_HERMETIC=1), we build libnixl from source and the wheel
478
+ isn't required.
479
+ """
480
+ if not shutil.which("meson"):
481
+ return False, "meson not on PATH (apt install meson)"
482
+ if not shutil.which("ninja"):
483
+ return False, "ninja not on PATH (apt install ninja-build)"
484
+ if not shutil.which("nvcc"):
485
+ return False, (
486
+ "nvcc not on PATH (install CUDA toolkit and put "
487
+ "/usr/local/cuda/bin on $PATH); needed for nixl_ep CUDA kernels"
488
+ )
489
+ if not shutil.which("git"):
490
+ return False, "git not on PATH; needed for `git apply` of patch overlays"
491
+ pkgconfig = shutil.which("pkg-config")
492
+ if not pkgconfig:
493
+ return False, "pkg-config not on PATH (apt install pkg-config)"
494
+ r = subprocess.run([pkgconfig, "--exists", "ucx"], capture_output=True)
495
+ if r.returncode:
496
+ return False, (
497
+ "UCX not found via pkg-config (no ucx.pc); "
498
+ "build UCX from source or set PKG_CONFIG_PATH"
499
+ )
500
+ # libibverbs is needed by NIXL's UCX + EP transports.
501
+ r = subprocess.run([pkgconfig, "--exists", "libibverbs"], capture_output=True)
502
+ if r.returncode:
503
+ return False, "libibverbs not found via pkg-config (apt install libibverbs-dev)"
504
+ if not _flag("BUILD_NIXL_EP_HERMETIC"):
505
+ if _find_nixl_wheel_lib_dir() is None:
506
+ return False, (
507
+ "nixl pip wheel not importable (or libnixl.so missing); install with "
508
+ "`uv pip install --no-deps 'nixl-cu13>=1.0.1'` "
509
+ "or set BUILD_NIXL_EP_HERMETIC=1 to build the full NIXL tree"
510
+ )
511
+ return True, ""
512
+
513
+
514
+ def _nccl_buildable() -> tuple[bool, str]:
515
+ """Probe for hard NCCL-EP build-time deps. Returns (ok, reason_if_not).
516
+
517
+ In the default (wheel-driven) flow, contrib/nccl_ep links against the
518
+ nvidia-nccl-cu13 pip wheel — so it must be importable. In hermetic
519
+ mode (BUILD_NCCL_EP_HERMETIC=1), we build libnccl from source and the
520
+ wheel isn't required.
521
+ """
522
+ if not shutil.which("make"):
523
+ return False, "make not on PATH (apt install build-essential)"
524
+ if not shutil.which("nvcc"):
525
+ return False, (
526
+ "nvcc not on PATH (install CUDA toolkit and put "
527
+ "/usr/local/cuda/bin on $PATH)"
528
+ )
529
+ if not shutil.which("git"):
530
+ return False, "git not on PATH; needed for `git apply` of patch overlays"
531
+ if not _flag("BUILD_NCCL_EP_HERMETIC"):
532
+ if _find_nccl_wheel_root() is None:
533
+ return False, (
534
+ "nvidia-nccl-cu13 pip wheel not importable; install with "
535
+ "`uv pip install --no-deps 'nvidia-nccl-cu13>=2.30.4'` "
536
+ "or set BUILD_NCCL_EP_HERMETIC=1 to build libnccl from source"
537
+ )
538
+ return True, ""
539
+
540
+
541
+ def _install_nvep_runtime_wheels(built_nixl: bool, built_nccl: bool) -> None:
542
+ """Install the EP-related runtime wheels with --no-deps, gated per backend.
543
+
544
+ These wheels supply the BASE libraries (libnccl.so.2, libnixl.so) that the
545
+ EP plugins (libnccl_ep.so, nixl_ep_cpp.so) dynamically load at runtime.
546
+ We do NOT stage the base libs into the FlashInfer package tree — relying
547
+ on these pip wheels keeps the wheel small and avoids the duplication.
548
+
549
+ The wheels carry transitive constraints (e.g. an nvidia-nccl-cu12 pin via
550
+ the `nixl` meta-package) that conflict with a recent torch and force a
551
+ downgrade when resolved normally. SGLang's Dockerfile mirrors this with
552
+ `pip install nixl nixl-cu13 --no-deps`; we do the same.
553
+
554
+ Two pip backends are tried, in order:
555
+ 1. `uv pip install` — works in venvs created by `uv venv` (which have
556
+ no pip module). This is the path most users hit.
557
+ 2. `python -m pip install` — for venvs with pip seeded.
558
+
559
+ Each wheel is gated on what was ACTUALLY built (not what was requested),
560
+ so `pip list` stays honest when a backend was skipped due to missing
561
+ build-time deps in best-effort mode.
562
+
563
+ This step is now FATAL on failure. Since we no longer stage the base
564
+ libs, a half-installed env where the wheels failed to install would
565
+ leave the EP plugins unable to load at runtime. Better to fail loudly
566
+ at install time.
567
+ """
568
+ cuda_major = _detect_cuda_major()
569
+ wheels: list[str] = []
570
+ if built_nixl:
571
+ wheels.append(f"nixl-cu{cuda_major}>=1.0.1")
572
+ if built_nccl:
573
+ wheels.append(f"nvidia-nccl-cu{cuda_major}>=2.30.4")
574
+ if not wheels:
575
+ return
576
+
577
+ print(f"[BUILD_NVEP] installing runtime wheels --no-deps: {' '.join(wheels)}")
578
+
579
+ # Prefer uv (works in a uv-created venv that has no pip module).
580
+ uv_bin = shutil.which("uv")
581
+ if uv_bin:
582
+ cmd = [
583
+ uv_bin,
584
+ "pip",
585
+ "install",
586
+ "--python",
587
+ sys.executable,
588
+ "--no-deps",
589
+ *wheels,
590
+ ]
591
+ print(f"[BUILD_NVEP] $ {' '.join(cmd)}")
592
+ subprocess.run(cmd, check=True)
593
+ return
594
+
595
+ # Fall back to python -m pip (requires pip in the venv).
596
+ cmd = [sys.executable, "-m", "pip", "install", "--no-deps", *wheels]
597
+ print(f"[BUILD_NVEP] $ {' '.join(cmd)}")
598
+ try:
599
+ subprocess.run(cmd, check=True)
600
+ except subprocess.CalledProcessError as e:
601
+ raise RuntimeError(
602
+ "Failed to install moe_ep runtime wheels and `uv` is not on "
603
+ "PATH. Either install uv (https://docs.astral.sh/uv/) so the "
604
+ "build hook can use `uv pip install`, or `--seed` your venv so "
605
+ "it has a pip module. The wheels we tried to install: "
606
+ f"{wheels}"
607
+ ) from e
608
+
609
+
610
+ def _gate_backend(name: str, requested: bool, probe) -> bool:
611
+ """Decide whether to actually build `name` given its requested flag.
612
+
613
+ Returns True if the backend should be built, False if it should be
614
+ skipped. Raises RuntimeError if the user explicitly asked for this
615
+ backend (not via the legacy BUILD_NVEP=1 alias) and a build-time dep
616
+ is missing.
617
+ """
618
+ if not requested:
619
+ return False
620
+ ok, reason = probe()
621
+ if ok:
622
+ return True
623
+ msg = f"[BUILD_NVEP] {name}: build skipped — {reason}"
624
+ if _BUILD_NVEP_BEST_EFFORT:
625
+ print(msg)
626
+ return False
627
+ # User opted in explicitly (BUILD_NCCL_EP=1 or BUILD_NIXL_EP=1) — fail hard.
628
+ raise RuntimeError(
629
+ f"{name} build requested but a hard dep is missing: {reason}. "
630
+ "Either install the missing dependency, or use BUILD_NVEP=1 "
631
+ "(best-effort mode) to skip this backend with a warning instead "
632
+ "of failing the install."
633
+ )
634
+
635
+
636
+ def _build_nvep_if_enabled() -> None:
637
+ if not (_BUILD_NCCL_EP or _BUILD_NIXL_EP):
638
+ return
639
+
640
+ requested = [
641
+ b for b, on in (("NIXL-EP", _BUILD_NIXL_EP), ("NCCL-EP", _BUILD_NCCL_EP)) if on
642
+ ]
643
+ mode = "best-effort" if _BUILD_NVEP_BEST_EFFORT else "strict"
644
+ print(f"[BUILD_NVEP] requested: {', '.join(requested)} (mode: {mode})")
645
+
646
+ # Pre-flight gating — probe each backend's hard build-time deps.
647
+ will_build_nixl = _gate_backend("NIXL-EP", _BUILD_NIXL_EP, _nixl_buildable)
648
+ will_build_nccl = _gate_backend("NCCL-EP", _BUILD_NCCL_EP, _nccl_buildable)
649
+
650
+ if not (will_build_nixl or will_build_nccl):
651
+ print("[BUILD_NVEP] nothing to build after pre-flight probe; skipping")
652
+ return
653
+
654
+ # Make sure each backend's submodule is initialized. Only fetch what we
655
+ # actually need — saves ~300MB and a network round-trip per backend.
656
+ # Guarded on `.git` because an sdist install has no git metadata: the
657
+ # submodule trees travel inside the sdist as plain directories and
658
+ # `git submodule update` would fail with "not a git repository".
659
+ in_git_repo = (_root / ".git").exists()
660
+ if will_build_nixl and not (_root / "3rdparty/nixl/meson.build").exists():
661
+ if not in_git_repo:
662
+ raise RuntimeError(
663
+ "3rdparty/nixl/meson.build is missing and this is not a git "
664
+ "checkout (likely an sdist install where the submodule wasn't "
665
+ "packaged). Either install from a git clone or fetch the "
666
+ "submodule tree manually into 3rdparty/nixl."
667
+ )
668
+ subprocess.run(
669
+ ["git", "submodule", "update", "--init", "--recursive", "3rdparty/nixl"],
670
+ cwd=_root,
671
+ check=True,
672
+ )
673
+ if will_build_nccl and not (_root / "3rdparty/nccl/Makefile").exists():
674
+ if not in_git_repo:
675
+ raise RuntimeError(
676
+ "3rdparty/nccl/Makefile is missing and this is not a git "
677
+ "checkout (likely an sdist install where the submodule wasn't "
678
+ "packaged). Either install from a git clone or fetch the "
679
+ "submodule tree manually into 3rdparty/nccl."
680
+ )
681
+ subprocess.run(
682
+ ["git", "submodule", "update", "--init", "--recursive", "3rdparty/nccl"],
683
+ cwd=_root,
684
+ check=True,
685
+ )
686
+
687
+ # Actual builds. If best-effort and the build raises despite the probe
688
+ # passing, swallow the error so the other backend still has a chance.
689
+ overall_t0 = time.monotonic()
690
+ built_nixl = False
691
+ if will_build_nixl:
692
+ try:
693
+ with _time_phase("_build_nixl_ep"):
694
+ _build_nixl_ep()
695
+ built_nixl = True
696
+ except Exception as e:
697
+ if not _BUILD_NVEP_BEST_EFFORT:
698
+ raise
699
+ print(f"[BUILD_NVEP] NIXL-EP build failed in best-effort mode: {e}")
700
+
701
+ built_nccl = False
702
+ if will_build_nccl:
703
+ try:
704
+ with _time_phase("_build_nccl_ep"):
705
+ _build_nccl_ep()
706
+ built_nccl = True
707
+ except Exception as e:
708
+ if not _BUILD_NVEP_BEST_EFFORT:
709
+ raise
710
+ print(f"[BUILD_NVEP] NCCL-EP build failed in best-effort mode: {e}")
711
+
712
+ if built_nixl or built_nccl:
713
+ with _time_phase("_fix_rpaths"):
714
+ _fix_rpaths()
715
+ with _time_phase("_install_nvep_runtime_wheels"):
716
+ _install_nvep_runtime_wheels(built_nixl=built_nixl, built_nccl=built_nccl)
717
+
718
+ print(
719
+ f"[BUILD_NVEP] total build phase wall time: "
720
+ f"{time.monotonic() - overall_t0:.1f}s",
721
+ flush=True,
722
+ )
723
+
724
+ built = [b for b, on in (("NIXL-EP", built_nixl), ("NCCL-EP", built_nccl)) if on]
725
+ print(f"[BUILD_NVEP] done — built: {', '.join(built) if built else 'nothing'}")
726
+
727
+
28
728
  def _create_build_metadata():
29
729
  """Create build metadata file with version information."""
30
730
  version_file = _root / "version.txt"
@@ -112,6 +812,7 @@ def _create_data_dir(use_symlinks=True):
112
812
 
113
813
  def _prepare_for_wheel():
114
814
  # For wheel, copy actual files instead of symlinks so they are included in the wheel
815
+ _build_nvep_if_enabled()
115
816
  if _data_dir.exists():
116
817
  shutil.rmtree(_data_dir)
117
818
  _create_data_dir(use_symlinks=False)
@@ -128,6 +829,7 @@ def _prepare_for_wheel():
128
829
 
129
830
  def _prepare_for_editable():
130
831
  # For editable install, use symlinks so changes are reflected immediately
832
+ _build_nvep_if_enabled()
131
833
  if _data_dir.exists():
132
834
  shutil.rmtree(_data_dir)
133
835
  _create_data_dir(use_symlinks=True)
@@ -135,6 +837,8 @@ def _prepare_for_editable():
135
837
 
136
838
  def _prepare_for_sdist():
137
839
  # For sdist, copy actual files instead of symlinks so they are included in the tarball
840
+ # NOTE: do NOT build moe_ep here — submodules + patches travel in the sdist
841
+ # itself and get built during the *install* of the sdist.
138
842
  if _data_dir.exists():
139
843
  shutil.rmtree(_data_dir)
140
844
  _create_data_dir(use_symlinks=False)