diff --git a/cpp/CMakeLists.txt b/cpp/CMakeLists.txt index 5dec92599ae6..3064cdc3bdc0 100644 --- a/cpp/CMakeLists.txt +++ b/cpp/CMakeLists.txt @@ -158,9 +158,29 @@ else() message(FATAL_ERROR "Failed to determine TensorRT LLM version") endif() +# Where the configured executor/version.h is written. Out-of-tree builds point +# this at the build tree so the source tree is never written (it may be mounted +# read-only); the default keeps the in-source location. +set(TRTLLM_VERSION_H_INCLUDE_DIR + "${CMAKE_CURRENT_SOURCE_DIR}/include" + CACHE PATH "Include root for the configured executor/version.h") configure_file( cmake/templates/version.h - ${CMAKE_CURRENT_SOURCE_DIR}/include/tensorrt_llm/executor/version.h) + "${TRTLLM_VERSION_H_INCLUDE_DIR}/tensorrt_llm/executor/version.h") +if(NOT TRTLLM_VERSION_H_INCLUDE_DIR STREQUAL + "${CMAKE_CURRENT_SOURCE_DIR}/include") + # BEFORE: the configured header must win over a stale in-source copy. + include_directories(BEFORE "${TRTLLM_VERSION_H_INCLUDE_DIR}") +endif() + +# Generated FMHA sources (see contextFusedMultiHeadAttention/CMakeLists.txt for +# the full definition and default). fmha_cubin.h is included transitively by +# headers used across several targets, so when generation is redirected out of +# the source tree the directory must be on every target's include path, not just +# the FMHA target's. +if(DEFINED TRTLLM_FMHA_GEN_DIR AND NOT "${TRTLLM_FMHA_GEN_DIR}" STREQUAL "") + include_directories("${TRTLLM_FMHA_GEN_DIR}") +endif() setup_cuda_compiler() diff --git a/cpp/kernels/fmha_v2/setup.py b/cpp/kernels/fmha_v2/setup.py index b574509ed3a9..e4e9f4be0e55 100644 --- a/cpp/kernels/fmha_v2/setup.py +++ b/cpp/kernels/fmha_v2/setup.py @@ -2394,7 +2394,10 @@ def get_kernel_code(kspec, kname, lname): params_str = 'reinterpret_cast(params)' if generate_cu_trtllm else 'params' attn_mask_type_str = 'using Attention_mask_type = ContextAttentionMaskType;' if generate_cu_trtllm else 'using Attention_mask_type = fmha::Attention_mask_type;' bert_launch_params = '' if generate_cu_trtllm else 'using Launch_params = bert::Fused_multihead_attention_launch_params;' - include_str = '#include "../fused_multihead_attention_common.h"\n' if generate_cu_trtllm else '' + # No "../" prefix: the generated .cu may live outside the source tree + # (TRTLLM_FMHA_GEN_DIR); the consuming CMake target puts the + # contextFusedMultiHeadAttention directory on the include path. + include_str = '#include "fused_multihead_attention_common.h"\n' if generate_cu_trtllm else '' include_str += '#include "tensorrt_llm/common/config.h"' if generate_cu_trtllm else '' num_compute_groups_str = '' if generate_cu_trtllm else 'static constexpr int NUM_COMPUTE_GROUPS = 2;' fused_multihead_attention_params_v2_str = 'Fused_multihead_attention_params_v2' if generate_cu_trtllm else f'{params_type}' diff --git a/cpp/tensorrt_llm/kernels/contextFusedMultiHeadAttention/CMakeLists.txt b/cpp/tensorrt_llm/kernels/contextFusedMultiHeadAttention/CMakeLists.txt index ce076e893ae0..4e179ce73b05 100644 --- a/cpp/tensorrt_llm/kernels/contextFusedMultiHeadAttention/CMakeLists.txt +++ b/cpp/tensorrt_llm/kernels/contextFusedMultiHeadAttention/CMakeLists.txt @@ -15,9 +15,31 @@ # the License. # +# Directory holding the generated FMHA sources (fmha_v2_cu/*.cu and +# cubin/fmha_cubin.{h,cpp}). scripts/build_wheel.py points this at an +# out-of-tree location when --out-of-tree is set; by default the generated files +# live in this directory as before. +set(TRTLLM_FMHA_GEN_DIR + "${CMAKE_CURRENT_SOURCE_DIR}" + CACHE PATH "Directory containing generated FMHA sources") + file(GLOB_RECURSE SRC_CPP *.cpp) file(GLOB_RECURSE SRC_CU *.cu) list(FILTER SRC_CU EXCLUDE REGEX "fmha_v2_cu/.*") +# fmha_cubin.cpp is generated; always take it from TRTLLM_FMHA_GEN_DIR so an +# out-of-tree copy is used when configured and a stale in-tree copy is never +# compiled alongside it. +list(FILTER SRC_CPP EXCLUDE REGEX "cubin/fmha_cubin\\.cpp$") +if(EXISTS "${TRTLLM_FMHA_GEN_DIR}/cubin/fmha_cubin.cpp") + list(APPEND SRC_CPP "${TRTLLM_FMHA_GEN_DIR}/cubin/fmha_cubin.cpp") +else() + message( + FATAL_ERROR + "Generated FMHA source ${TRTLLM_FMHA_GEN_DIR}/cubin/fmha_cubin.cpp is " + "missing. It is produced by the FMHA generation step " + "(scripts/build_wheel.py runs it before CMake); a missing file means " + "generation did not complete. Regenerate before configuring.") +endif() # The skip_softmax sm_120/sm_121 warp-specialized FMHA is a hand-written TU that # only compiles for the sm_120 family; it is added explicitly to the sm_120 arch # target below, not to the all-arch source list. @@ -31,6 +53,9 @@ filter_source_cuda_architectures( IMPLICIT_FAMILY) target_sources(context_attention_src PRIVATE ${SRC_CPP} ${SRC_CU}) +# For #include "cubin/fmha_cubin.h" when the generated header is out of tree. +target_include_directories(context_attention_src + PRIVATE "${TRTLLM_FMHA_GEN_DIR}") target_compile_definitions(context_attention_src PRIVATE USE_DEMO_BERT_PARAMS=1 GENERATE_CUBIN=1) set_target_properties( @@ -68,7 +93,7 @@ foreach(arch IN ITEMS 80 86 89 90 100 120) continue() endif() - file(GLOB arch_files "fmha_v2_cu/*_sm${arch}.cu") + file(GLOB arch_files "${TRTLLM_FMHA_GEN_DIR}/fmha_v2_cu/*_sm${arch}.cu") # Compile the hand-written skip_softmax warp-specialized FMHA into the sm_120 # family target. It uses sm_120-only TMA + sync-MMA, so it must never be # compiled for other architectures. @@ -94,12 +119,28 @@ foreach(arch IN ITEMS 80 86 89 90 100 120) set_target_properties( ${TARGET_NAME} PROPERTIES POSITION_INDEPENDENT_CODE ON CUDA_RESOLVE_DEVICE_SYMBOLS ON) + # CMAKE_CURRENT_SOURCE_DIR resolves the generated kernels' #include + # "fused_multihead_attention_common.h" when they live out of tree (and is + # where includer-relative lookup lands in-tree). target_include_directories( ${TARGET_NAME} - PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/../../../kernels/fmha_v2/src/ + PRIVATE ${CMAKE_CURRENT_SOURCE_DIR} + ${CMAKE_CURRENT_SOURCE_DIR}/../../../kernels/fmha_v2/src/ ${CMAKE_CURRENT_SOURCE_DIR}/../../../kernels/fmha_v2/generated/) set_cuda_architectures(${TARGET_NAME} ${TARGET_ARCH}) target_sources(context_attention_src PUBLIC $) + else() + # arch is enabled (disabled archs already `continue`d above) but the + # generation step produced no *_sm${arch}.cu kernels for it. Fail rather + # than silently building without this architecture's FMHA kernels, which + # would surface only as a missing-kernel failure at runtime. (arch 120 + # always has the hand-written skip_softmax TU appended above, so this branch + # covers the generated-only architectures.) + message( + FATAL_ERROR + "No generated FMHA kernels (*_sm${arch}.cu) found in " + "${TRTLLM_FMHA_GEN_DIR}/fmha_v2_cu for enabled architecture ${arch}. " + "The FMHA generation step did not produce sources for this arch.") endif() endforeach() diff --git a/docs/source/installation/build-from-source.md b/docs/source/installation/build-from-source.md index c4fbdb270ad2..a6d66c7f3d58 100644 --- a/docs/source/installation/build-from-source.md +++ b/docs/source/installation/build-from-source.md @@ -109,7 +109,7 @@ With `--build_root ` set, the following default under `` instead of th Conan's `cpp/CMakeUserPresets.json` convenience file is also skipped in this mode, since it would reference the (possibly ephemeral) out-of-tree build directory. -Only final artifacts are still written into the checkout: `tensorrt_llm/libs`, `tensorrt_llm/include`, Python bindings and stubs, generated FMHA kernel sources, and the `.whl` output directory (`--dist_dir`). +Only final artifacts are still written into the checkout: `tensorrt_llm/libs`, `tensorrt_llm/include`, Python bindings and stubs, generated FMHA kernel sources, the configured `cpp/include/tensorrt_llm/executor/version.h`, and the `.whl` output directory (`--dist_dir`). Related knobs for shared-storage workflows: @@ -119,6 +119,16 @@ Related knobs for shared-storage workflows: Plain local-disk builds are unaffected: without `--build_root`, all paths behave as before. +#### Out-of-tree wheel builds (read-only checkout) + +For CI or ephemeral-node workflows that only need a wheel, add `--out-of-tree` to guarantee the checkout is never written — it can even be mounted read-only: + +```bash +python3 scripts/build_wheel.py --build_root /tmp/trtllm-build --out-of-tree --use_ccache -a "90-real" +``` + +In this mode the generated FMHA kernel sources and the configured `version.h` go to the build tree (via the `TRTLLM_FMHA_GEN_DIR` and `TRTLLM_VERSION_H_INCLUDE_DIR` CMake variables), and the wheel is assembled from a staging copy of the Python package under `/package`, landing in `/dist` by default. Submodules and git-lfs content must be materialized before the build (the usual `git submodule update --init --recursive`), since the build will not modify the checkout. Editable-install workflows (`--skip_building_wheel`, `--linking_install_binary`, `--install`) are incompatible with `--out-of-tree`: they import compiled artifacts from the checkout by design. `--version-override` is also incompatible, since it edits `tensorrt_llm/version.py` in the checkout. + ### Python-only build (no C++ compilation) If you only need to modify Python code, you can skip C++ compilation entirely by reusing precompiled binaries: diff --git a/scripts/build_wheel.py b/scripts/build_wheel.py index 18151f5d5e3f..4a9f9ee02f21 100755 --- a/scripts/build_wheel.py +++ b/scripts/build_wheel.py @@ -95,11 +95,20 @@ def get_source_dir(): return get_project_dir() / "cpp" -def get_build_dir(build_dir, build_type, build_root=None): +def get_build_dir(build_dir, build_type, build_root=None, out_of_tree=False): if build_dir is None: dir_name = "build" if build_type == "Release" else f"build_{build_type}" if build_root is not None: - build_dir = Path(build_root).resolve() / f"cpp-{dir_name}" + # Isolate out-of-tree build state in its own CMake directory so + # that toggling --out-of-tree against an existing conventional + # build under the same --build_root always triggers a fresh + # configure (first_build) instead of reusing a CMakeCache whose + # redirected FMHA/version.h paths don't match the mode. Without + # this, switching modes without --clean/--configure_cmake would + # skip configure and silently write into the checkout despite + # --out-of-tree. + suffix = "-oot" if out_of_tree else "" + build_dir = Path(build_root).resolve() / f"cpp-{dir_name}{suffix}" else: build_dir = get_source_dir() / dir_name else: @@ -306,12 +315,38 @@ def _fmha_generation_stamp(fmha_v2_cu_dir: Path) -> Path: return fmha_v2_cu_dir / ".generation_complete" -def generate_fmha_cu(project_dir, venv_python): - fmha_v2_cu_dir = project_dir / "cpp/tensorrt_llm/kernels/contextFusedMultiHeadAttention/fmha_v2_cu" +def get_fmha_gen_dirs(project_dir, gen_root=None): + """Return (fmha_v2_cu_dir, cubin_dir) for generated FMHA sources. + + gen_root=None keeps the historical in-source locations; otherwise both + live under gen_root (consumed by CMake via TRTLLM_FMHA_GEN_DIR). + """ + base = (project_dir / + "cpp/tensorrt_llm/kernels/contextFusedMultiHeadAttention" + if gen_root is None else gen_root) + return base / "fmha_v2_cu", base / "cubin" + + +def generate_fmha_cu(project_dir, venv_python, gen_root=None): + fmha_v2_cu_dir, cubin_dir = get_fmha_gen_dirs(project_dir, gen_root) fmha_v2_cu_dir.mkdir(parents=True, exist_ok=True) + cubin_dir.mkdir(parents=True, exist_ok=True) _fmha_generation_stamp(fmha_v2_cu_dir).unlink(missing_ok=True) fmha_v2_dir = project_dir / "cpp/kernels/fmha_v2" + if gen_root is not None: + # The generator writes ./generated, ./temp and ./obj relative to its + # own directory; run it from a scratch copy so a (possibly read-only) + # checkout is never written. + work_dir = gen_root / "fmha_v2-work" + if work_dir.exists(): + rmtree(work_dir) + copytree(fmha_v2_dir, + work_dir, + symlinks=False, + ignore=shutil.ignore_patterns("generated", "temp", "obj", + "__pycache__")) + fmha_v2_dir = work_dir env = os.environ.copy() env.update({ @@ -344,7 +379,6 @@ def move_if_updated(src, dst): shutil.move(src, dst) # Copy generated header file when cu path is active and cubins are deleted. - cubin_dir = project_dir / "cpp/tensorrt_llm/kernels/contextFusedMultiHeadAttention/cubin" move_if_updated(fmha_v2_dir / "generated/fmha_cubin.h", cubin_dir / "fmha_cubin.h") @@ -545,6 +579,36 @@ def build_kv_cache_manager_v2(project_dir, print("-- Done building kv_cache_manager_v2.") +def stage_python_package(project_dir: Path, staging_dir: Path) -> None: + """Copy the sources setup.py packages into an out-of-tree staging project. + + Out-of-tree builds install compiled artifacts into this staging tree and + build the wheel from it, so nothing is ever written into the checkout. + """ + print(f"-- Staging python package sources into {staging_dir} ...") + staging_dir.mkdir(parents=True, exist_ok=True) + ignore = shutil.ignore_patterns("__pycache__", "*.pyc") + # examples: setup.py's root-level find_packages() ships the + # examples.configs.database package from it. + for tree in ("tensorrt_llm", "triton_kernels", "examples", + "3rdparty/MSA/python/fmha_sm100"): + dst = staging_dir / tree + if dst.exists(): + rmtree(dst) + copytree(project_dir / tree, dst, symlinks=False, ignore=ignore) + top_level_files = [ + "setup.py", "pyproject.toml", "requirements.txt", + "requirements-dev.txt", "constraints.txt", "LICENSE", "README.md" + ] + top_level_files += [ + f.name for f in project_dir.glob("ATTRIBUTIONS-CPP-*.md") + ] + for name in top_level_files: + src = project_dir / name + if src.exists(): + copy(src, staging_dir / name) + + def main(*, build_type: str = "Release", generator: str = "", @@ -564,6 +628,7 @@ def main(*, configure_cmake: bool = False, configure_only: bool = False, use_ccache: bool = False, + out_of_tree: bool = False, use_3rdparty_cache: bool = False, fast_build: bool = False, cpp_only: bool = False, @@ -586,7 +651,6 @@ def main(*, clean_wheel = True project_dir = get_project_dir() - apply_version_override(project_dir, version_override) # Out-of-tree build state: everything metadata-heavy (venv, wheel # staging, ccache, intermediate objects) goes under build_root, keeping @@ -604,6 +668,26 @@ def main(*, os.environ.setdefault("TRTLLM_WHEEL_STAGING_DIR", str(build_root / "wheel-staging")) + if out_of_tree: + # Out-of-tree: never write into the checkout; the wheel is assembled in + # an out-of-tree staging project. Validated by building with the + # checkout mounted read-only. + if build_root is None: + raise RuntimeError("--out-of-tree requires --build_root") + if platform.system() == "Windows": + raise RuntimeError("--out-of-tree is not supported on Windows") + if skip_building_wheel or linking_install_binary or install: + raise RuntimeError( + "--out-of-tree is incompatible with --skip_building_wheel, " + "--linking_install_binary and --install: editable installs " + "import compiled artifacts from the checkout, which a " + "out-of-tree build never writes.") + if version_override: + raise RuntimeError( + "--out-of-tree does not support --version-override (it would " + "modify tensorrt_llm/version.py in the checkout)") + + apply_version_override(project_dir, version_override) os.chdir(project_dir) # Get all submodules and check their folder exists. If not, @@ -613,8 +697,16 @@ def main(*, l.split("=")[1].strip() for l in submodules_f.readlines() if "path = " in l ] - if any(not (project_dir / submodule / ".git").exists() - for submodule in submodules): + missing_submodules = [ + s for s in submodules if not (project_dir / s / ".git").exists() + ] + if missing_submodules: + if out_of_tree: + raise RuntimeError( + "Missing submodules: " + ", ".join(missing_submodules) + + ". Run 'git submodule update --init --recursive' before a " + "out-of-tree build; the checkout is not modified during the " + "build.") build_run('git submodule update --init --recursive') on_windows = platform.system() == "Windows" requirements_filename = "requirements-dev-windows.txt" if on_windows else "requirements-dev.txt" @@ -692,7 +784,7 @@ def main(*, raise RuntimeError("Mooncake is not supported on Windows.") cmake_def_args.append(f"-DMOONCAKE_ROOT={mooncake_root}") - build_dir = get_build_dir(build_dir, build_type, build_root) + build_dir = get_build_dir(build_dir, build_type, build_root, out_of_tree) first_build = not Path(build_dir, "CMakeFiles").exists() if clean and build_dir.exists(): @@ -771,10 +863,18 @@ def main(*, source_dir = get_source_dir() - fmha_v2_cu_dir = project_dir / "cpp/tensorrt_llm/kernels/contextFusedMultiHeadAttention/fmha_v2_cu" + fmha_gen_root = build_root / "fmha-gen" if out_of_tree else None + fmha_v2_cu_dir, _ = get_fmha_gen_dirs(project_dir, fmha_gen_root) if (clean or generate_fmha or not _fmha_generation_stamp(fmha_v2_cu_dir).exists()): - generate_fmha_cu(project_dir, venv_python) + generate_fmha_cu(project_dir, venv_python, fmha_gen_root) + + if out_of_tree: + cmake_def_args.append(f"-DTRTLLM_FMHA_GEN_DIR={fmha_gen_root}") + # Write the configured executor/version.h into the build tree + # instead of cpp/include (the checkout may be read-only). + cmake_def_args.append( + f"-DTRTLLM_VERSION_H_INCLUDE_DIR={build_dir}/generated-include") with working_directory(build_dir): if clean or first_build or configure_cmake or configure_only: @@ -823,7 +923,15 @@ def main(*, assert not install, "Installing is not supported for cpp_only builds" return - pkg_dir = project_dir / "tensorrt_llm" + if out_of_tree: + # Assemble the wheel in an out-of-tree staging project; the checkout + # is only read from this point on. + wheel_project_dir = build_root / "package" + stage_python_package(project_dir, wheel_project_dir) + else: + wheel_project_dir = project_dir + + pkg_dir = wheel_project_dir / "tensorrt_llm" assert pkg_dir.is_dir(), f"{pkg_dir} is not a directory" lib_dir = pkg_dir / "libs" include_dir = pkg_dir / "include" @@ -832,7 +940,7 @@ def main(*, if include_dir.exists(): clear_folder(include_dir) # Remove auto-generated attributions file from previous builds - auto_attr_file = project_dir / "ATTRIBUTIONS.md" + auto_attr_file = wheel_project_dir / "ATTRIBUTIONS.md" if auto_attr_file.exists(): os.remove(auto_attr_file) @@ -1072,9 +1180,8 @@ def copy_resolving_symlink(src_path, dst_path): agent_binding_so = list( nixl_utils_dir.glob("tensorrt_llm_transfer_agent_binding*.so")) if agent_binding_so: - trtllm_dir = project_dir / "tensorrt_llm" install_file(agent_binding_so[0], - trtllm_dir / agent_binding_so[0].name) + pkg_dir / agent_binding_so[0].name) if os.path.exists( build_dir / "tensorrt_llm/executor/cache_transmission/mooncake_utils/libtensorrt_llm_mooncake_wrapper.so" @@ -1183,14 +1290,14 @@ def get_binding_lib(subdirectory, name): nixl_root is not None or mooncake_root is not None, binding_lib_file_name) - build_kv_cache_manager_v2(project_dir, + build_kv_cache_manager_v2(wheel_project_dir, venv_python, use_mypyc=mypyc, build_root=build_root) if not skip_building_wheel: if dist_dir is None: - dist_dir = project_dir / "build" + dist_dir = build_root / "dist" if out_of_tree else project_dir / "build" else: dist_dir = Path(dist_dir) @@ -1249,13 +1356,13 @@ def get_binding_lib(subdirectory, name): # Copy auto-generated ATTRIBUTIONS.md to project root for wheel packaging if auto_attr.exists(): - install_file(auto_attr, project_dir / "ATTRIBUTIONS.md") + install_file(auto_attr, wheel_project_dir / "ATTRIBUTIONS.md") print( - f"Copied auto-generated attributions to {project_dir / 'ATTRIBUTIONS.md'}" + f"Copied auto-generated attributions to {wheel_project_dir / 'ATTRIBUTIONS.md'}" ) build_run( - f'\"{venv_python}\" -m build {project_dir} --skip-dependency-check {extra_wheel_build_args} --no-isolation --wheel --outdir "{dist_dir}"' + f'\"{venv_python}\" -m build {wheel_project_dir} --skip-dependency-check {extra_wheel_build_args} --no-isolation --wheel --outdir "{dist_dir}"' ) env = os.environ.copy() if mypyc: @@ -1264,7 +1371,7 @@ def get_binding_lib(subdirectory, name): env["TRTLLM_ENABLE_MYPYC"] = "0" build_run( - f'\"{venv_python}\" -m build {project_dir} --skip-dependency-check {plat_name_arg} --no-isolation --wheel --outdir "{dist_dir}"', + f'\"{venv_python}\" -m build {wheel_project_dir} --skip-dependency-check {plat_name_arg} --no-isolation --wheel --outdir "{dist_dir}"', env=env) if install: @@ -1378,6 +1485,17 @@ def add_arguments(parser: ArgumentParser): "directory instead of the checkout. Point it at fast local storage (e.g. " "/tmp) when the checkout lives on a network filesystem. Individual " "options like --build_dir and CCACHE_DIR still override their piece.") + parser.add_argument( + "--out-of-tree", + action="store_true", + help= + "Never write into the checkout: generated FMHA sources and version.h " + "go to the build tree, and the wheel is assembled from an out-of-tree " + "staging copy of the Python package (default wheel output: " + "/dist). Requires --build_root; the checkout may be " + "mounted read-only. Incompatible with editable-install workflows " + "(--skip_building_wheel, --linking_install_binary, --install) and " + "with --version-override (it edits tensorrt_llm/version.py in place).") parser.add_argument( "--build_dir", type=Path,