From 9dbc5b38baba399c5517685ecc5b66f57a177a4c Mon Sep 17 00:00:00 2001 From: Kaiyu Xie <26294424+kaiyux@users.noreply.github.com> Date: Thu, 4 Jul 2024 14:37:19 +0800 Subject: [PATCH] Update TensorRT-LLM (#1891) * Update TensorRT-LLM --------- Co-authored-by: Marks101 Co-authored-by: lkm2835 --- README.md | 7 +- benchmarks/cpp/gptManagerBenchmark.cpp | 122 +- benchmarks/cpp/gptSessionBenchmark.cpp | 5 +- benchmarks/python/allowed_configs.py | 4 - benchmarks/python/build.py | 31 - .../tensorrt_llm_bench/benchmarkers/static.py | 9 +- benchmarks/suite/tensorrt_llm_bench/ifb.py | 2 +- .../tensorrt_llm/batch_manager/llmRequest.h | 52 +- .../batch_manager/trtGptModelOptionalParams.h | 5 +- cpp/include/tensorrt_llm/executor/executor.h | 13 +- cpp/include/tensorrt_llm/executor/types.h | 4 +- cpp/include/tensorrt_llm/runtime/gptDecoder.h | 12 +- .../tensorrt_llm/runtime/gptDecoderBatch.h | 17 +- .../tensorrt_llm/runtime/iGptDecoderBatch.h | 6 +- .../tensorrt_llm/runtime/modelConfig.h | 20 +- cpp/micro_benchmarks/README.md | 2 +- .../gen-moe-benchmark-file.py | 7 +- .../mixtureOfExpertsBackendBenchmarkFixture.h | 188 ++- ...ixtureOfExpertsBackendBenchmarkLauncher.cu | 221 +-- .../libtensorrt_llm_batch_manager_static.a | 4 +- ...sorrt_llm_batch_manager_static.pre_cxx11.a | 4 +- .../aarch64-linux-gnu/version.txt | 6 +- .../libtensorrt_llm_batch_manager_static.a | 4 +- ...sorrt_llm_batch_manager_static.pre_cxx11.a | 4 +- .../tensorrt_llm_batch_manager_static.lib | 4 +- cpp/tensorrt_llm/common/envUtils.cpp | 13 +- cpp/tensorrt_llm/common/envUtils.h | 4 +- cpp/tensorrt_llm/common/mpiUtils.cpp | 27 +- .../libtensorrt_llm_executor_static.a | 4 +- ...ibtensorrt_llm_executor_static.pre_cxx11.a | 4 +- .../executor/aarch64-linux-gnu/version.txt | 6 +- .../libtensorrt_llm_executor_static.a | 4 +- ...ibtensorrt_llm_executor_static.pre_cxx11.a | 4 +- .../tensorrt_llm_executor_static.lib | 4 +- cpp/tensorrt_llm/kernels/beamSearchKernels.h | 3 +- .../beamSearchKernelsTemplate.h | 394 ++--- .../kernels/customAllReduceKernels.cu | 26 +- .../kernels/decoderMaskedMultiheadAttention.h | 2 +- .../decoderMaskedMultiheadAttentionTemplate.h | 6 +- .../decoderXQAImplJIT/compileEngine.cpp | 5 +- .../decoderXQAImplJIT/cubinObj.cpp | 53 + .../decoderXQAImplJIT/cubinObj.h | 5 +- .../decoderXQAImplJIT/decoderXQAImplJIT.cpp | 76 +- .../decoderXQAImplJIT/decoderXQAImplJIT.h | 8 +- .../decoderXQAImplJIT/kernelUtils.cpp | 171 ++ .../decoderXQAImplJIT/kernelUtils.h | 31 + .../libtensorrt_llm_nvrtc_wrapper.so | 4 +- .../aarch64-linux-gnu/version.txt | 4 +- .../nvrtcWrapper/include/nvrtcWrapper.h | 10 + .../libtensorrt_llm_nvrtc_wrapper.so | 4 +- .../tensorrt_llm_nvrtc_wrapper.dll | 4 +- .../tensorrt_llm_nvrtc_wrapper.lib | 2 +- .../decoderXQAImplPrecompiled.cpp | 71 +- .../decoderXQARunner.cpp | 12 +- .../decoderXQARunner.h | 73 +- ...decoderMaskedMultiheadAttention160_bf16.cu | 2 +- .../tensorMapUtils.cpp | 12 +- .../decoderMaskedMultiheadAttentionUtils.h | 36 +- cpp/tensorrt_llm/kernels/decodingKernels.cu | 3 +- .../kernels/mixtureOfExperts/moe_kernels.h | 17 + .../kernels/samplingAirTopPKernels.cu | 128 +- .../medusaDecodingKernels.cu | 3 +- .../kernels/weightOnlyBatchedGemv/fp8Gemm.cu | 142 +- .../kernels/weightOnlyBatchedGemv/fp8Gemm.h | 16 +- cpp/tensorrt_llm/layers/beamSearchLayer.cu | 167 +- cpp/tensorrt_llm/layers/beamSearchLayer.h | 30 +- cpp/tensorrt_llm/layers/decodingLayer.cpp | 2 +- .../layers/dynamicDecodeLayer.cpp | 2 +- .../bertAttentionPlugin.cpp | 12 +- .../bertAttentionPlugin/bertAttentionPlugin.h | 2 +- .../plugins/gemmPlugin/gemmPlugin.cpp | 45 +- .../gptAttentionCommon/gptAttentionCommon.cpp | 14 +- .../gptAttentionCommon/gptAttentionCommon.h | 4 +- .../gptAttentionPlugin/gptAttentionPlugin.cpp | 38 +- .../gptAttentionPlugin/gptAttentionPlugin.h | 6 +- .../plugins/loraPlugin/loraPlugin.cpp | 38 +- .../plugins/ncclPlugin/allreducePlugin.cpp | 7 +- cpp/tensorrt_llm/pybind/executor/bindings.cpp | 47 +- cpp/tensorrt_llm/runtime/gptDecoder.cpp | 27 +- cpp/tensorrt_llm/runtime/gptDecoderBatch.cpp | 219 ++- cpp/tensorrt_llm/runtime/gptJsonConfig.cpp | 16 +- cpp/tensorrt_llm/runtime/tllmRuntime.cpp | 2 +- cpp/tensorrt_llm/runtime/worldConfig.cpp | 37 +- cpp/tests/common/mpiUtilsTest.cpp | 11 +- .../kernels/fp8Gemm/fp8GemmKernelTest.cpp | 55 +- .../data/test_model_lora_config.json | 1 - .../scripts/build_enc_dec_engines.py | 4 +- .../scripts/generate_expected_gptj_output.py | 2 +- cpp/tests/resources/scripts/test_cpp.py | 2 + docs/source/advanced/gpt-runtime.md | 2 +- docs/source/advanced/weight-streaming.md | 2 +- .../installation/build-from-source-windows.md | 8 +- docs/source/installation/windows.md | 4 +- docs/source/media/picture-07-02-2024.png | Bin 0 -> 689064 bytes .../source/performance/perf-best-practices.md | 4 + docs/source/reference/precision.md | 2 +- docs/source/reference/support-matrix.md | 4 +- examples/apps/README.md | 20 +- examples/apps/chat.py | 122 +- examples/apps/fastapi_server.py | 134 +- examples/apps/requirements.txt | 1 + examples/baichuan/requirements.txt | 2 +- examples/bert/README.md | 3 - examples/bert/build.py | 5 - examples/bloom/requirements.txt | 2 +- examples/chatglm/requirements.txt | 2 +- examples/dbrx/requirements.txt | 2 +- examples/enc_dec/README.md | 35 +- examples/enc_dec/convert_checkpoint.py | 40 +- examples/enc_dec/helper.py | 2 +- examples/falcon/requirements.txt | 2 +- examples/gemma/requirements.txt | 3 +- examples/gpt/README.md | 39 + examples/gpt/requirements.txt | 2 +- examples/gptj/requirements.txt | 2 +- examples/gptneox/requirements.txt | 2 +- examples/grok/requirements.txt | 2 +- examples/hf_lora_convert.py | 23 +- examples/high-level-api/README.md | 91 +- examples/high-level-api/llm_examples.py | 62 +- examples/high-level-api/quickstart_example.py | 19 + examples/high-level-api/requirements.txt | 2 +- examples/internlm/requirements.txt | 2 +- examples/llama/README.md | 7 +- examples/llama/requirements.txt | 2 +- examples/mamba/requirements.txt | 2 +- examples/medusa/requirements.txt | 2 +- examples/mixtral/requirements.txt | 2 +- examples/mmlu.py | 55 +- examples/model_api/README.md | 2 +- examples/model_api/llama.py | 18 +- examples/model_api/llama_multi_gpu.py | 42 +- examples/model_api/llama_quantize.py | 17 +- examples/mpt/requirements.txt | 2 +- examples/multimodal/README.md | 100 +- examples/multimodal/build_visual_engine.py | 24 +- examples/multimodal/run.py | 23 +- examples/nemotron/requirements.txt | 2 +- .../openai_triton/manual_plugin/README.md | 30 + examples/opt/convert_checkpoint.py | 18 +- examples/opt/requirements.txt | 2 +- examples/phi/README.md | 51 +- examples/phi/requirements.txt | 2 +- examples/quantization/requirements.txt | 2 +- examples/qwen/README.md | 67 + examples/qwen/requirements.txt | 2 +- examples/qwenvl/requirements.txt | 2 +- examples/recurrentgemma/requirements.txt | 2 +- examples/run.py | 45 +- examples/skywork/requirements.txt | 2 +- examples/smaug/requirements.txt | 2 +- examples/summarize.py | 12 +- examples/utils.py | 26 + examples/whisper/README.md | 8 +- examples/whisper/requirements.txt | 2 +- requirements-windows.txt | 8 +- requirements.txt | 7 +- tensorrt_llm/__init__.py | 6 +- tensorrt_llm/auto_parallel/cluster_info.py | 61 +- tensorrt_llm/builder.py | 26 +- tensorrt_llm/commands/build.py | 88 +- tensorrt_llm/executor.py | 309 ++-- tensorrt_llm/functional.py | 32 +- tensorrt_llm/hlapi/__init__.py | 17 +- tensorrt_llm/hlapi/_perf_evaluator.py | 47 +- tensorrt_llm/hlapi/build_cache.py | 272 ++++ tensorrt_llm/hlapi/llm.py | 1220 +++------------ tensorrt_llm/hlapi/llm_utils.py | 1376 +++++++++++++++++ tensorrt_llm/hlapi/tokenizer.py | 43 +- tensorrt_llm/hlapi/utils.py | 46 +- tensorrt_llm/layers/attention.py | 1 - tensorrt_llm/layers/moe.py | 198 ++- tensorrt_llm/logger.py | 5 +- tensorrt_llm/lora_manager.py | 13 + tensorrt_llm/models/convert_utils.py | 1 + tensorrt_llm/models/gpt/model.py | 13 +- tensorrt_llm/models/gptneox/model.py | 6 +- tensorrt_llm/models/llama/config.py | 3 + tensorrt_llm/models/llama/convert.py | 4 +- tensorrt_llm/models/llama/model.py | 6 +- tensorrt_llm/models/modeling_utils.py | 27 +- tensorrt_llm/models/opt/model.py | 6 +- tensorrt_llm/models/phi3/model.py | 25 +- tensorrt_llm/models/qwen/model.py | 17 +- tensorrt_llm/plugin/plugin.py | 2 - tensorrt_llm/quantization/functional.py | 4 + tensorrt_llm/quantization/layers.py | 24 +- tensorrt_llm/quantization/quantize.py | 2 +- .../quantization/quantize_by_modelopt.py | 1 + tensorrt_llm/runtime/generation.py | 71 +- tensorrt_llm/runtime/model_runner_cpp.py | 101 +- tensorrt_llm/version.py | 2 +- tests/attention/test_gpt_attention.py | 83 +- tests/bindings/test_executor_bindings.py | 19 + tests/functional/test_moe.py | 27 +- tests/functional/test_scatter_nd.py | 85 + tests/hlapi/apps/README.md | 3 + tests/hlapi/apps/_test_llm_chat.py | 33 + tests/hlapi/apps/_test_llm_server.py | 47 + tests/hlapi/grid_searcher.py | 44 +- tests/hlapi/hlapi_evaluator.py | 73 +- tests/hlapi/run_llm.py | 7 +- tests/hlapi/test_build_cache.py | 125 ++ tests/hlapi/test_executor.py | 109 +- tests/hlapi/test_llm.py | 198 ++- tests/hlapi/test_llm_download.py | 38 +- tests/hlapi/test_llm_multi_gpu.py | 62 +- tests/hlapi/test_llm_perf_evaluator.py | 17 +- tests/hlapi/test_llm_quant.py | 26 +- tests/hlapi/test_llm_utils.py | 212 +++ tests/hlapi/test_mpi_session.py | 5 + tests/model_api/test_model_api_multi_gpu.py | 76 +- tests/model_api/test_model_level_api.py | 87 +- tests/model_api/test_model_quantization.py | 56 +- windows/setup_build_env.ps1 | 12 +- windows/setup_env.ps1 | 49 +- 216 files changed, 6178 insertions(+), 3563 deletions(-) create mode 100644 cpp/tensorrt_llm/kernels/decoderMaskedMultiheadAttention/decoderXQAImplJIT/kernelUtils.cpp create mode 100644 cpp/tensorrt_llm/kernels/decoderMaskedMultiheadAttention/decoderXQAImplJIT/kernelUtils.h create mode 100644 docs/source/media/picture-07-02-2024.png mode change 100644 => 100755 examples/apps/chat.py mode change 100644 => 100755 examples/apps/fastapi_server.py create mode 100644 examples/high-level-api/quickstart_example.py create mode 100644 tensorrt_llm/hlapi/build_cache.py create mode 100644 tensorrt_llm/hlapi/llm_utils.py create mode 100644 tests/functional/test_scatter_nd.py create mode 100644 tests/hlapi/apps/README.md create mode 100644 tests/hlapi/apps/_test_llm_chat.py create mode 100644 tests/hlapi/apps/_test_llm_server.py create mode 100644 tests/hlapi/test_build_cache.py create mode 100644 tests/hlapi/test_llm_utils.py diff --git a/README.md b/README.md index df0e42f06..e9c8e89e4 100644 --- a/README.md +++ b/README.md @@ -7,7 +7,7 @@ TensorRT-LLM [![Documentation](https://img.shields.io/badge/docs-latest-brightgreen.svg?style=flat)](https://nvidia.github.io/TensorRT-LLM/) [![python](https://img.shields.io/badge/python-3.10.12-green)](https://www.python.org/downloads/release/python-31012/) [![cuda](https://img.shields.io/badge/cuda-12.4.1-green)](https://developer.nvidia.com/cuda-downloads) -[![trt](https://img.shields.io/badge/TRT-10.0.1-green)](https://developer.nvidia.com/tensorrt) +[![trt](https://img.shields.io/badge/TRT-10.1.0-green)](https://developer.nvidia.com/tensorrt) [![version](https://img.shields.io/badge/release-0.11.0.dev-green)](./tensorrt_llm/version.py) [![license](https://img.shields.io/badge/license-Apache%202-blue)](./LICENSE) @@ -17,7 +17,10 @@ TensorRT-LLM
## Latest News -* [*Weekly*] Check out **[@NVIDIAAIDev](https://twitter.com/nvidiaaidev?lang=en)** & **[NVIDIA AI](https://www.linkedin.com/showcase/nvidia-ai/)** LinkedIn for the latest updates! +* [2024/07/02] Let the @MistralAI MoE tokens fly 📈 🚀 #Mixtral 8x7B with NVIDIA #TensorRT #LLM on #H100. +[➡️ Tech blog](https://developer.nvidia.com/blog/achieving-high-mixtral-8x7b-performance-with-nvidia-h100-tensor-core-gpus-and-tensorrt-llm?ncid=so-twit-928467) +![Example Image](docs/source/media/picture-07-02-2024.png) + * [2024/02/06] [🚀 Speed up inference with SOTA quantization techniques in TRT-LLM](./docs/source/blogs/quantization-in-TRT-LLM.md) * [2024/01/30] [ New XQA-kernel provides 2.4x more Llama-70B throughput within the same latency budget](./docs/source/blogs/XQA-kernel.md) * [2023/12/04] [Falcon-180B on a single H200 GPU with INT4 AWQ, and 6.7x faster Llama-70B over A100](./docs/source/blogs/Falcon180B-H200.md) diff --git a/benchmarks/cpp/gptManagerBenchmark.cpp b/benchmarks/cpp/gptManagerBenchmark.cpp index eeb7da68d..f171fd0b7 100644 --- a/benchmarks/cpp/gptManagerBenchmark.cpp +++ b/benchmarks/cpp/gptManagerBenchmark.cpp @@ -150,7 +150,9 @@ struct BenchmarkParams bool streaming{false}; bool enableExpDelays{false}; std::optional requestRate{std::nullopt}; + std::optional concurrency{std::nullopt}; std::optional maxBatchSize{std::nullopt}; + std::optional maxNumTokens{std::nullopt}; int randomSeed = 430; std::optional maxAttentionWindow{std::nullopt}; @@ -773,7 +775,9 @@ class ExecutorServer : mRecorder(std::move(recorder)) , mWaitSleep(waitSleep) , mStaticEmulatedBatchSize(staticEmulatedBatchSize) + , mConcurrency(benchmarkParams.concurrency) , mActiveCount(0) + , mNumFinished(0) , mShutdown(false) { @@ -793,6 +797,10 @@ class ExecutorServer { executorConfig.setMaxBatchSize(benchmarkParams.maxBatchSize.value()); } + if (benchmarkParams.maxNumTokens) + { + executorConfig.setMaxNumTokens(benchmarkParams.maxNumTokens.value()); + } executorConfig.setDecodingConfig(texec::DecodingConfig( benchmarkParams.medusaChoices.has_value() ? texec::DecodingMode::Medusa() : texec::DecodingMode::Auto(), @@ -843,10 +851,19 @@ class ExecutorServer } } + void resetNumFinished() + { + mNumFinished = 0; + } + + bool canEnqueue(int numSentRequests) const + { + return !mConcurrency || (numSentRequests - mNumFinished < mConcurrency); + } + void waitForResponses(SizeType32 numRequests, bool warmup = false) { - SizeType32 numFinished = 0; - while (mActiveCount || (numFinished < numRequests)) + while (mActiveCount || (mNumFinished < numRequests)) { auto responses = mExecutor->awaitResponses(mWaitSleep); for (auto const& response : responses) @@ -856,7 +873,7 @@ class ExecutorServer if (response.getResult().isFinal) { mActiveCount--; - numFinished++; + mNumFinished++; if (!warmup) { mRecorder->recordEnd(reqId, response); @@ -873,7 +890,7 @@ class ExecutorServer } } - void collectStats() + void collectStats() const { while (!mShutdown) { @@ -893,7 +910,9 @@ class ExecutorServer std::shared_ptr mRecorder; std::chrono::milliseconds mWaitSleep; std::optional mStaticEmulatedBatchSize; + std::optional mConcurrency; std::atomic mActiveCount; + std::atomic mNumFinished; std::atomic mShutdown; }; // class ExecutorServer @@ -914,9 +933,7 @@ class GptServer , mInferReqSyncSndHdl(nullptr) { auto const jsonConfig = GptJsonConfig::parse(trtEnginePath / "config.json"); - SizeType32 deviceCount{0}; - TLLM_CUDA_CHECK(cudaGetDeviceCount(&deviceCount)); - mWorldConfig = WorldConfig::mpi(deviceCount, jsonConfig.getTensorParallelism(), + mWorldConfig = WorldConfig::mpi(jsonConfig.getGpusPerNode(), jsonConfig.getTensorParallelism(), jsonConfig.getPipelineParallelism(), optionalParams.deviceIds); auto& comm = COMM_SESSION; mCommTensorParallel = std::make_shared( @@ -1352,16 +1369,15 @@ void benchmarkGptManager(std::filesystem::path const& engineDir, TrtGptModelType optionalParams.gpuWeightsPercent = benchmarkParams.gpuWeightsPercent; optionalParams.maxBeamWidth = beamWidth; optionalParams.maxBatchSize = benchmarkParams.maxBatchSize; + optionalParams.maxNumTokens = benchmarkParams.maxNumTokens; optionalParams.schedulerConfig = texec::SchedulerConfig{capacitySchedulerPolicy}; optionalParams.decodingConfig = texec::DecodingConfig( benchmarkParams.medusaChoices.has_value() ? texec::DecodingMode::Medusa() : texec::DecodingMode::Auto(), std::nullopt, benchmarkParams.medusaChoices); auto const jsonConfig = GptJsonConfig::parse(engineDir / "config.json"); - SizeType32 deviceCount{0}; - TLLM_CUDA_CHECK(cudaGetDeviceCount(&deviceCount)); - auto const worldConfig = WorldConfig::mpi( - deviceCount, jsonConfig.getTensorParallelism(), jsonConfig.getPipelineParallelism(), optionalParams.deviceIds); + auto const worldConfig = WorldConfig::mpi(jsonConfig.getGpusPerNode(), jsonConfig.getTensorParallelism(), + jsonConfig.getPipelineParallelism(), optionalParams.deviceIds); BufferManager bufferManager{std::make_shared()}; // the stream is not used @@ -1551,53 +1567,49 @@ void benchmarkExecutor(std::filesystem::path const& engineDir, TrtGptModelType m benchmarkParams.streaming, returnContextLogits, returnGenerationLogits, loraConfig)); } - bool hasDelay + bool const hasDelay = std::any_of(timeDelays.begin(), timeDelays.end(), [](auto const& delay) { return delay > 0.0; }); - if (hasDelay && staticEmulatedBatchSize) + executorServer->resetNumFinished(); + if (!staticEmulatedBatchSize) { - TLLM_THROW("Executor benchmark doesn't support delays with emulated static batch sizes"); - } + // Launch a thread that will wait for responses + std::thread waitThread( + [numSamples, executorServer]() { executorServer->waitForResponses(numSamples); }); - if (!hasDelay) - { - if (!staticEmulatedBatchSize) - { - executorServer->enqueue(std::move(requests)); - executorServer->waitForResponses(numSamples); - } - else + // Enqueue requests one by one + int numSentRequests = 0; + while (numSentRequests < numSamples) { - SizeType32 numRequests = requests.size(); - SizeType32 maxBatchSize = staticEmulatedBatchSize.value(); - for (SizeType32 req = 0; req < numRequests; req += maxBatchSize) + if (executorServer->canEnqueue(numSentRequests)) { - auto batchSize = std::min(maxBatchSize, numRequests - req); - - std::vector requestsBatch(std::make_move_iterator(requests.begin() + req), - std::make_move_iterator(requests.begin() + req + batchSize)); - // Enqueue in batches - executorServer->enqueue(std::move(requestsBatch)); - // Wait for current batch to be done - executorServer->waitForResponses(batchSize); + executorServer->enqueue({requests.at(numSentRequests)}); + if (hasDelay && numSentRequests < numSamples - 1) + { + std::this_thread::sleep_for( + std::chrono::milliseconds(static_cast(timeDelays.at(numSentRequests) * 1000))); + } + numSentRequests += 1; } } + waitThread.join(); } else { - // Launch a thread that will wait for responses - std::thread waitThread( - [numSamples, executorServer]() { executorServer->waitForResponses(numSamples); }); - // Enqueue requests one by one - for (std::size_t i = 0; i < numSamples; ++i) + TLLM_CHECK_WITH_INFO( + !hasDelay, "Executor benchmark doesn't support delays with emulated static batch sizes"); + SizeType32 numRequests = requests.size(); + SizeType32 maxBatchSize = staticEmulatedBatchSize.value(); + for (SizeType32 req = 0; req < numRequests; req += maxBatchSize) { - executorServer->enqueue({std::move(requests.at(i))}); - if (i < numSamples - 1) - { - std::this_thread::sleep_for( - std::chrono::milliseconds(static_cast(timeDelays.at(i) * 1000))); - } + auto batchSize = std::min(maxBatchSize, numRequests - req); + + std::vector requestsBatch(std::make_move_iterator(requests.begin() + req), + std::make_move_iterator(requests.begin() + req + batchSize)); + // Enqueue in batches + executorServer->enqueue(std::move(requestsBatch)); + // Wait for current batch to be done + executorServer->waitForResponses(batchSize); } - waitThread.join(); } } recorder->finalize(); @@ -1670,7 +1682,10 @@ int main(int argc, char* argv[]) options.add_options()("request_rate", "request rate in reqs/sec. Skipping this arg or negative value will trigger offline/0-delay.", cxxopts::value()); + options.add_options()("concurrency", "Concurrent number of connections with the server.", cxxopts::value()); options.add_options()("max_batch_size", "The max runtime batch size when benchmarking", cxxopts::value()); + options.add_options()( + "max_num_tokens", "The max runtime number of tokens per batch when benchmarking", cxxopts::value()); options.add_options()("enable_trt_overlap", "Overlap TRT context preparation and execution", cxxopts::value()->default_value("false")); options.add_options()("enable_exp_delays", "Enables exponential delay distr to mimic real world request arrival", @@ -1816,18 +1831,33 @@ int main(int argc, char* argv[]) // Argument: streaming benchmarkParams.streaming = result["streaming"].as(); + TLLM_CHECK_WITH_INFO(!(result.count("request_rate") && result.count("concurrency")), + "request_rate and concurrency cannot be specified at the same time."); + // Argument: request rate if (result.count("request_rate")) { benchmarkParams.requestRate = result["request_rate"].as(); } + // Argument: concurrency + if (result.count("concurrency")) + { + benchmarkParams.concurrency = result["concurrency"].as(); + } + // Argument: request rate if (result.count("max_batch_size")) { benchmarkParams.maxBatchSize = result["max_batch_size"].as(); } + // Argument: request rate + if (result.count("max_num_tokens")) + { + benchmarkParams.maxNumTokens = result["max_num_tokens"].as(); + } + benchmarkParams.enableExpDelays = result["enable_exp_delays"].as(); // Argument: Enable batch stats output diff --git a/benchmarks/cpp/gptSessionBenchmark.cpp b/benchmarks/cpp/gptSessionBenchmark.cpp index b4eced7f9..f29e0a239 100644 --- a/benchmarks/cpp/gptSessionBenchmark.cpp +++ b/benchmarks/cpp/gptSessionBenchmark.cpp @@ -75,9 +75,8 @@ void benchmarkGptSession(std::filesystem::path const& dataPath, std::vector auto const json = GptJsonConfig::parse(jsonFileName); auto const modelConfig = json.getModelConfig(); auto const inputPacked = modelConfig.usePackedInput(); - SizeType32 deviceCount{0}; - TLLM_CUDA_CHECK(cudaGetDeviceCount(&deviceCount)); - auto const worldConfig = WorldConfig::mpi(deviceCount, json.getTensorParallelism(), json.getPipelineParallelism()); + auto const worldConfig + = WorldConfig::mpi(json.getGpusPerNode(), json.getTensorParallelism(), json.getPipelineParallelism()); auto& comm = COMM_SESSION; auto const enginePath = dataPath / json.engineFilename(worldConfig); auto const dtype = modelConfig.getDataType(); diff --git a/benchmarks/python/allowed_configs.py b/benchmarks/python/allowed_configs.py index 45cf050c5..3b0550abb 100644 --- a/benchmarks/python/allowed_configs.py +++ b/benchmarks/python/allowed_configs.py @@ -41,7 +41,6 @@ class BuildConfig: type_vocab_size: Optional[int] = None pre_norm: Optional[bool] = None do_layer_norm_before: Optional[bool] = None - enable_qk_half_accum: bool = False enable_context_fmha: bool = True enable_multi_block_mode: bool = False # The enum name of PositionEmbeddingType @@ -651,7 +650,6 @@ class ModelConfig: max_batch_size=256, max_input_len=512, builder_opt=None, - enable_qk_half_accum=False, enable_context_fmha=False, )), "bert_large": @@ -669,7 +667,6 @@ class ModelConfig: max_batch_size=64, max_input_len=512, builder_opt=None, - enable_qk_half_accum=False, enable_context_fmha=False, )), "roberta_base": @@ -687,7 +684,6 @@ class ModelConfig: max_batch_size=64, max_input_len=512, builder_opt=None, - enable_qk_half_accum=False, enable_context_fmha=False, )), "falcon_rw_1b": diff --git a/benchmarks/python/build.py b/benchmarks/python/build.py index c5fb39888..7adcc0dfa 100644 --- a/benchmarks/python/build.py +++ b/benchmarks/python/build.py @@ -264,21 +264,6 @@ def build_gpt(args): max_input_len = build_config['max_input_len'] \ if args.max_input_len is None else args.max_input_len - if args.max_output_len: - logger.warning( - '--max_output_len has been deprecated in favor of --max_seq_len') - if args.max_input_len: - if args.max_seq_len: - logger.warning( - '--max_seq_len has been overwritten due to --max_output_len being specified' - ) - args.max_seq_len = args.max_input_len + args.max_output_len - else: - raise Exception( - f"max_output_len is specified but not max_input_len") - - del args.max_output_len - max_seq_len = build_config['max_seq_len'] \ if args.max_seq_len is None else args.max_seq_len max_beam_width = build_config['max_beam_width'] \ @@ -1113,7 +1098,6 @@ def build_bert(args): if args.mode == 'plugin': network.plugin_config.bert_attention_plugin = args.dtype network.plugin_config.gemm_plugin = args.dtype - network.plugin_config.attention_qk_half_accumulation = True network.plugin_config.set_context_fmha(ContextFMHAType.enabled) elif args.mode == 'ootb-except-mha': network.plugin_config.bert_attention_plugin = args.dtype @@ -1573,21 +1557,6 @@ def build_enc_dec(args): if args.max_input_len is None else args.max_input_len build_config['max_decoder_input_len'] = 1 - if args.max_output_len: - logger.warning( - '--max_output_len has been deprecated in favor of --max_seq_len') - if args.max_input_len: - if args.max_seq_len: - logger.warning( - '--max_seq_len has been overwritten due to --max_output_len being specified' - ) - args.max_seq_len = args.max_input_len + args.max_output_len - else: - raise Exception( - f"max_output_len is specified but not max_input_len") - - del args.max_output_len - build_config['max_seq_len'] = build_config['max_seq_len'] \ if args.max_seq_len is None else args.max_seq_len build_config[ diff --git a/benchmarks/suite/tensorrt_llm_bench/benchmarkers/static.py b/benchmarks/suite/tensorrt_llm_bench/benchmarkers/static.py index b2e67d06f..b5b3c49ea 100644 --- a/benchmarks/suite/tensorrt_llm_bench/benchmarkers/static.py +++ b/benchmarks/suite/tensorrt_llm_bench/benchmarkers/static.py @@ -1,3 +1,4 @@ +import platform from pathlib import Path from subprocess import CompletedProcess from typing import Dict, List @@ -143,11 +144,9 @@ def benchmark(self): """Benchmarks a TRT-LLM for a configured instance.""" # Compile the command for running - cmd = [ - "mpirun", - "-allow-run-as-root", - "-n", - self.config.world_size, + cmd = ["mpiexec", "-n", self.config.world_size] + cmd += ["-allow-run-as-root"] if platform.system() != "Windows" else "" + cmd += [ self.gpt_session_path, "--engine_dir", self.config.engine_path, diff --git a/benchmarks/suite/tensorrt_llm_bench/ifb.py b/benchmarks/suite/tensorrt_llm_bench/ifb.py index cb12c04ad..67299c082 100644 --- a/benchmarks/suite/tensorrt_llm_bench/ifb.py +++ b/benchmarks/suite/tensorrt_llm_bench/ifb.py @@ -285,7 +285,7 @@ def executor_benchmark( # the logger.info("Launching benchmark...") bench_cmd = \ - ["mpirun", "-n", f"{benchmark_cfg.world_size}", "python"] + \ + ["mpiexec", "-n", f"{benchmark_cfg.world_size}", "python"] + \ sys.argv + ["--run"] process = subprocess.Popen( bench_cmd, diff --git a/cpp/include/tensorrt_llm/batch_manager/llmRequest.h b/cpp/include/tensorrt_llm/batch_manager/llmRequest.h index be4d5fa57..eb959234f 100644 --- a/cpp/include/tensorrt_llm/batch_manager/llmRequest.h +++ b/cpp/include/tensorrt_llm/batch_manager/llmRequest.h @@ -84,6 +84,7 @@ class GenericLlmRequest , mSamplingConfig(samplingConfig) , mState(REQUEST_STATE_CONTEXT_INIT) , mIsStreaming(isStreaming) + , mReturnAllGeneratedTokens(isStreaming && (samplingConfig.beamWidth > 1)) , mEndId(endId) , mPadId(padId) , mLogitsPostProcessor(logitsPostProcessor) @@ -126,6 +127,7 @@ class GenericLlmRequest , mSamplingConfig(req.getSamplingConfig(), req.getExternalDraftTokensConfig()) , mState(REQUEST_STATE_CONTEXT_INIT) , mIsStreaming(req.getStreaming()) + , mReturnAllGeneratedTokens(req.getReturnAllGeneratedTokens()) , mEndId(req.getEndId()) , mPadId(req.getPadId()) , mOrigPromptLen(mPromptLen) @@ -152,6 +154,16 @@ class GenericLlmRequest , mReturnEncoderOutput(req.getOutputConfig().returnEncoderOutput) , mDecodingIter(0) { + if (mIsStreaming && mSamplingConfig.beamWidth > 1 && mReturnAllGeneratedTokens == false) + { + TLLM_LOG_WARNING( + "Setting mReturnAllGeneratedTokens to True since streaming AND beam search are done simultaneously. " + "Returning the full beams at each streaming step is needed because beam search + streaming can change " + "previous outputs. Initialize request with mReturnAllGeneratedTokens = True to dismiss this error." + "WARNING: using this option may increase network usage significantly (quadratically w.r.t output " + "length)."); + mReturnAllGeneratedTokens = true; + } if (req.getEncoderInputTokenIds()) { mState = REQUEST_STATE_ENCODER_INIT; @@ -224,7 +236,10 @@ class GenericLlmRequest if (mPromptLen > maxInputLen) { - TLLM_THROW("Prompt length (%d) exceeds maximum input length (%d).", mPromptLen, maxInputLen); + TLLM_THROW( + "Prompt length (%d) exceeds maximum input length (%d). Set log level to info and check " + "TRTGptModel logs for how maximum input length is set", + mPromptLen, maxInputLen); } // Maximum number of draft tokens per request we pass to the engine for single runtime iteration. @@ -243,8 +258,13 @@ class GenericLlmRequest if (mPromptLen + draftLenPerEngineStep > maxInputLen) { - TLLM_THROW("Prompt length + number of draft tokens (%d + %d) exceeds maximum input length (%d).", - mPromptLen, draftLenPerEngineStep, maxInputLen); + auto const newDraftLenPerEngineStep = maxInputLen - mPromptLen; + TLLM_LOG_WARNING( + "Prompt length + number of draft tokens (%d + %d) exceeds maximum input length (%d)." + "Number of draft tokens is changed to (%d)", + mPromptLen, draftLenPerEngineStep, maxInputLen, newDraftLenPerEngineStep); + draftLenPerEngineStep = newDraftLenPerEngineStep; + mDraftTokens->resize(draftLenPerEngineStep); } } @@ -570,6 +590,15 @@ class GenericLlmRequest return mDraftTokens->size(); } + void discardDraftTokens(SizeType32 numTokensToDiscard) + { + TLLM_CHECK_WITH_INFO( + numTokensToDiscard > 0, "Can only discard a positive amount of draft tokens, got %d", numTokensToDiscard); + TLLM_CHECK_WITH_INFO(numTokensToDiscard <= getNumDraftTokens(), + "Can't discard more draft tokens (%d) than exists (%d).", numTokensToDiscard, getNumDraftTokens()); + mDraftTokens->resize(getNumDraftTokens() - numTokensToDiscard); + } + void setNumTokensPerIteration(SizeType32 numTokensPerIteration) { mNumTokensPerIteration = numTokensPerIteration; @@ -854,16 +883,22 @@ class GenericLlmRequest // FIXME(nkorobov): For streaming we do not allow beam search and // streaming index calculation here applies only for sampling // getNumTokensPerIteration takes accepted draft tokens into account - int nbTokensOut = mIsStreaming ? std::max(getNumTokensPerIteration(), 1) : maxNbTokens; + auto nbTokensOut + = (mReturnAllGeneratedTokens || !mIsStreaming) ? maxNbTokens : std::max(getNumTokensPerIteration(), 1); + if (mExcludeInputFromOutput && !mIsStreaming) { nbTokensOut -= getOrigPromptLen(); } result.outputTokenIds.resize(nbBeams); - SizeType32 tokenPos = maxNbTokens - nbTokensOut; - bool shouldSendResponse = isGenerationCompleteState() || (mIsStreaming && tokenPos > getMaxSentTokenPos()); + // in the case of streaming + beam search + // we need to return the full beams at all iterations + + SizeType32 tokenPos{maxNbTokens - nbTokensOut}; + auto const shouldSendResponse = isGenerationCompleteState() + || (mIsStreaming && tokenPos > getMaxSentTokenPos()) || mReturnAllGeneratedTokens; if (!shouldSendResponse) { @@ -874,7 +909,8 @@ class GenericLlmRequest for (SizeType32 beam = 0; beam < nbBeams; ++beam) { auto tokens = getTokens(beam); - auto nbTokens = mIsStreaming ? (tokenPos - getMaxSentTokenPos()) : tokens.size(); + auto nbTokens = (mReturnAllGeneratedTokens || !mIsStreaming) ? tokens.size() + : (tokenPos - getMaxSentTokenPos()); // Take accepted draft tokens into account when streaming auto const numAcceptedTokens = std::max(0, getNumTokensPerIteration() - 1); @@ -946,6 +982,8 @@ class GenericLlmRequest runtime::SamplingConfig mSamplingConfig; LlmRequestState_t mState; bool mIsStreaming; + // whether to return the full beams on each iteration. True when doing streaming + beamsearch + bool mReturnAllGeneratedTokens; std::optional mEndId; std::optional mPadId; std::optional mSeqSlot; diff --git a/cpp/include/tensorrt_llm/batch_manager/trtGptModelOptionalParams.h b/cpp/include/tensorrt_llm/batch_manager/trtGptModelOptionalParams.h index 77cd6d673..00211156f 100644 --- a/cpp/include/tensorrt_llm/batch_manager/trtGptModelOptionalParams.h +++ b/cpp/include/tensorrt_llm/batch_manager/trtGptModelOptionalParams.h @@ -42,6 +42,7 @@ class TrtGptModelOptionalParams PeftCacheManagerConfig const& peftCacheManagerConfig = PeftCacheManagerConfig{}, executor::DecodingConfig decodingConfig = executor::DecodingConfig{}, float gpuWeightsPercent = 1, std::optional maxBeamWidth = std::nullopt, std::optional maxBatchSize = std::nullopt, + std::optional maxNumTokens = std::nullopt, executor::SchedulerConfig const& schedulerConfig = executor::SchedulerConfig{}) : kvCacheConfig{kvCacheConfig} , enableTrtOverlap{enableTrtOverlap} @@ -53,6 +54,7 @@ class TrtGptModelOptionalParams , gpuWeightsPercent(gpuWeightsPercent) , maxBeamWidth(maxBeamWidth) , maxBatchSize(maxBatchSize) + , maxNumTokens(maxNumTokens) , schedulerConfig{schedulerConfig} { } @@ -64,7 +66,7 @@ class TrtGptModelOptionalParams PeftCacheManagerConfig(executorConfig.getPeftCacheConfig().value_or(executor::PeftCacheConfig())), executorConfig.getDecodingConfig().value_or(executor::DecodingConfig{}), executorConfig.getGpuWeightsPercent(), executorConfig.getMaxBeamWidth(), executorConfig.getMaxBatchSize(), - executorConfig.getSchedulerConfig()) + executorConfig.getMaxNumTokens(), executorConfig.getSchedulerConfig()) { } @@ -89,6 +91,7 @@ class TrtGptModelOptionalParams float gpuWeightsPercent; std::optional maxBeamWidth; std::optional maxBatchSize; + std::optional maxNumTokens; executor::SchedulerConfig schedulerConfig; }; diff --git a/cpp/include/tensorrt_llm/executor/executor.h b/cpp/include/tensorrt_llm/executor/executor.h index e398e6d08..f154e7917 100644 --- a/cpp/include/tensorrt_llm/executor/executor.h +++ b/cpp/include/tensorrt_llm/executor/executor.h @@ -105,6 +105,7 @@ class SamplingConfig static std::optional const& checkTopPResetIds(std::optional const& topPResetIds); static std::optional const& checkTopPDecay(std::optional const& topPDecay); static std::optional const& checkTemperature(std::optional const& temperature); + static std::optional const& checkRepetitionPenalty(std::optional const& penalty); static std::optional const& checkMinLength(std::optional const& minLength); static std::optional const& checkNoRepeatNgramSize(std::optional const& noRepeatNgramSize); static std::optional const& checkBeamSearchDiversityRate( @@ -251,6 +252,8 @@ class Request /// @param logitsPostProcessorName The logits postprocessor name. Must correspond to one of the logits postprocessor /// name provided to the ExecutorConfig. /// @param encoderInputTokenIds The encoder input token ids for encoder-decoder models, or encoder-only models + /// @param returnAllGeneratedTokens Indicates whether to return the full beams or just the newly generated tokens + /// after every streaming step. Request(VecTokens inputTokenIds, SizeType32 maxNewTokens, bool streaming = false, SamplingConfig const& samplingConfig = SamplingConfig(), OutputConfig const& outputConfig = OutputConfig(), std::optional const& endId = std::nullopt, std::optional const& padId = std::nullopt, @@ -261,7 +264,7 @@ class Request std::optional pTuningConfig = std::nullopt, std::optional loraConfig = std::nullopt, std::optional logitsPostProcessorName = std::nullopt, - std::optional encoderInputTokenIds = std::nullopt); + std::optional encoderInputTokenIds = std::nullopt, bool returnAllGeneratedTokens = false); /// @brief This logits postprocessor name will dispatch to the batched logits postprocessor static auto constexpr kBatchedPostProcessorName = "batched"; @@ -287,6 +290,7 @@ class Request [[nodiscard]] std::optional getLoraConfig() const; [[nodiscard]] std::optional getLogitsPostProcessorName() const; [[nodiscard]] std::optional getEncoderInputTokenIds() const; + [[nodiscard]] bool getReturnAllGeneratedTokens() const; void setStreaming(bool streaming); void setSamplingConfig(SamplingConfig const& config); @@ -301,6 +305,7 @@ class Request void setLoraConfig(LoraConfig const& loraConfig); void setLogitsPostProcessorName(std::string const& logitsPostProcessorName); void setEncoderInputTokenIds(VecTokens const& encoderInputTokenIds); + void setReturnAllGeneratedTokens(bool returnAllGeneratedTokens); private: friend class Serialization; @@ -648,6 +653,7 @@ class ExecutorConfig bool normalizeLogProbs = true, SizeType32 iterStatsMaxIterations = kDefaultIterStatsMaxIterations, SizeType32 requestStatsMaxIterations = kDefaultRequestStatsMaxIterations, BatchingType batchingType = BatchingType::kINFLIGHT, std::optional maxBatchSize = std::nullopt, + std::optional maxNumTokens = std::nullopt, std::optional parallelConfig = std::nullopt, std::optional const& peftCacheConfig = std::nullopt, std::optional logitsPostProcessorMap = std::nullopt, @@ -663,6 +669,7 @@ class ExecutorConfig [[nodiscard]] SizeType32 getRequestStatsMaxIterations() const; [[nodiscard]] BatchingType getBatchingType() const; [[nodiscard]] std::optional getMaxBatchSize() const; + [[nodiscard]] std::optional getMaxNumTokens() const; [[nodiscard]] std::optional getParallelConfig() const; [[nodiscard]] std::optional getPeftCacheConfig() const; [[nodiscard]] std::optional getLogitsPostProcessorMap() const; @@ -672,6 +679,7 @@ class ExecutorConfig void setMaxBeamWidth(SizeType32 maxBeamWidth); void setMaxBatchSize(SizeType32 maxBatchSize); + void setMaxNumTokens(SizeType32 maxNumTokens); void setSchedulerConfig(SchedulerConfig const& schedulerConfig); void setKvCacheConfig(KvCacheConfig const& kvCacheConfig); void setEnableChunkedContext(bool enableChunkedContext); @@ -716,6 +724,9 @@ class ExecutorConfig /// @brief The max batch size of requests std::optional mMaxBatchSize; + /// @brief The max number of tokens per batch + std::optional mMaxNumTokens; + /// @brief The parallel execution configuration. std::optional mParallelConfig; std::optional mPeftCacheConfig; diff --git a/cpp/include/tensorrt_llm/executor/types.h b/cpp/include/tensorrt_llm/executor/types.h index 009707c56..2b6411c5c 100644 --- a/cpp/include/tensorrt_llm/executor/types.h +++ b/cpp/include/tensorrt_llm/executor/types.h @@ -279,6 +279,8 @@ struct IterationStats double iterLatencyMS; /// @brief Number of active requests SizeType32 numActiveRequests; + /// @brief Number of queued requests + SizeType32 numQueuedRequests; /// @brief Number of max active requests SizeType32 maxNumActiveRequests; /// @brief GPU memory usage in bytes @@ -375,7 +377,7 @@ class DecodingMode static auto constexpr Medusa() { - return DecodingMode{kMedusa | kUseMinLength | kStandardStopCriteria}; + return DecodingMode{kMedusa | kUseMinLength | kStandardStopCriteria | kUseExplicitEosStop}; } static auto constexpr Lookahead() diff --git a/cpp/include/tensorrt_llm/runtime/gptDecoder.h b/cpp/include/tensorrt_llm/runtime/gptDecoder.h index 136f8f32b..89509d957 100644 --- a/cpp/include/tensorrt_llm/runtime/gptDecoder.h +++ b/cpp/include/tensorrt_llm/runtime/gptDecoder.h @@ -49,8 +49,7 @@ class IGptDecoder virtual ~IGptDecoder() = default; - virtual void setup(SamplingConfig const& samplingConfig, size_t batchSize, - std::optional const& batchSlots = std::nullopt, + virtual void setup(SamplingConfig const& samplingConfig, size_t batchSize, SizeType32 const* batchSlots = nullptr, std::optional const& output = std::nullopt) = 0; @@ -59,7 +58,8 @@ class IGptDecoder virtual void forwardSync(DecodingOutput& output, DecodingInput const& input) = 0; virtual void gatherTree(ITensor& finalOutputIds, DecodingOutput const& decodingOutput, - DecodingInput const& decodingInput, BufferManager const& manager) + DecodingInput const& decodingInput, BufferManager const& manager, + std::optional> samplingConfig = std::nullopt) = 0; virtual SamplingConfig const& getSamplingConfig() = 0; @@ -92,8 +92,7 @@ class GptDecoder : public virtual IGptDecoder size_t vocabSizePadded, size_t maxSequenceLength, CudaStreamPtr const& stream, std::shared_ptr speculativeDecodingModule = nullptr); - void setup(SamplingConfig const& samplingConfig, size_t batchSize, - std::optional const& batchSlots = std::nullopt, + void setup(SamplingConfig const& samplingConfig, size_t batchSize, SizeType32 const* batchSlots = nullptr, std::optional const& output = std::nullopt) override; void forwardAsync(DecodingOutput& output, DecodingInput const& input) override; @@ -101,7 +100,8 @@ class GptDecoder : public virtual IGptDecoder void forwardSync(DecodingOutput& output, DecodingInput const& input) override; void gatherTree(ITensor& finalOutputIds, DecodingOutput const& decodingOutput, DecodingInput const& decodingInput, - BufferManager const& manager) override; + BufferManager const& manager, + std::optional> samplingConfig = std::nullopt) override; SamplingConfig const& getSamplingConfig() override { diff --git a/cpp/include/tensorrt_llm/runtime/gptDecoderBatch.h b/cpp/include/tensorrt_llm/runtime/gptDecoderBatch.h index cf230a649..a1517c1e4 100644 --- a/cpp/include/tensorrt_llm/runtime/gptDecoderBatch.h +++ b/cpp/include/tensorrt_llm/runtime/gptDecoderBatch.h @@ -95,9 +95,9 @@ class GptDecoderBatch : public IGptDecoderBatch return ITensor::slice(mJointDecodingOutput->ids, 0, mActualBatchSize); } - //! @brief Gather final beam search results for request `batchIdx`. + //! @brief Gather final beam search results for request `batchSlot`. //! Result will only be available after event returned. - [[nodiscard]] CudaEvent finalize(SizeType32 batchIdx) const override; + [[nodiscard]] CudaEvent finalize(SizeType32 batchSlot, SamplingConfig const& samplingConfig) const override; //! @brief Gather final beam search results for all requests. void finalize() const override; @@ -196,12 +196,18 @@ class GptDecoderBatch : public IGptDecoderBatch return mJointDecodingOutput->speculativeDecodingOutputs->pathsOffsets; } + executor::DecodingMode getDecodingMode() const override + { + return mDecodingMode; + } + private: //! @brief Gather final beam search results for request `batchIdx`. - [[nodiscard]] CudaEvent postProcessRequest(SizeType32 batchIdx) const; + [[nodiscard]] CudaEvent postProcessRequest(SizeType32 batchIdx, + std::optional> samplingConfig = std::nullopt) const; - //! @brief Initialize the decoder at `batchIdx` with a new `request`. - void newRequest(SizeType32 batchIdx, decoder_batch::Request const& request, SamplingConfig const& samplingConfig); + //! @brief Initialize the decoder at `batchSlot` with a new `request`. + void newRequest(SizeType32 batchSlot, decoder_batch::Request const& request, SamplingConfig const& samplingConfig); //! @brief Allocate buffers for speculative decoding. void allocateSpeculativeDecodingBuffers(); @@ -303,5 +309,6 @@ class GptDecoderBatch : public IGptDecoderBatch bool mFusedDecoder{false}; SpeculativeDecodingMode mSpeculativeDecodingMode; + executor::DecodingMode mDecodingMode{executor::DecodingMode::Auto()}; }; } // namespace tensorrt_llm::runtime diff --git a/cpp/include/tensorrt_llm/runtime/iGptDecoderBatch.h b/cpp/include/tensorrt_llm/runtime/iGptDecoderBatch.h index 5e7702e7f..2771635ba 100644 --- a/cpp/include/tensorrt_llm/runtime/iGptDecoderBatch.h +++ b/cpp/include/tensorrt_llm/runtime/iGptDecoderBatch.h @@ -76,7 +76,7 @@ class Input // within one beam for beam search, on gpu std::vector> predictedDraftLogits; // [maxBatchSize][maxAcceptedDraftTokensPerStep][maxDraftTokens + 1, vocabSizePadded] - TensorConstPtr seqSlots; // [batchSize] + TensorPtr seqSlots; // [batchSize] // explicit draft tokens data. std::optional explicitDraftTokensInputs; @@ -135,7 +135,7 @@ class IGptDecoderBatch : public virtual IStatefulGptDecoder //! @brief Gather final beam search results for request `batchIdx`. //! Result will only be available after event returned - [[nodiscard]] virtual CudaEvent finalize(SizeType32 batchIdx) const = 0; + [[nodiscard]] virtual CudaEvent finalize(SizeType32 batchIdx, SamplingConfig const& samplingConfig) const = 0; //! @returns [batchSize (actual)], marks finished requests (per batch) [[nodiscard]] virtual std::vector getFinished() const = 0; @@ -156,6 +156,8 @@ class IGptDecoderBatch : public virtual IStatefulGptDecoder [[nodiscard]] virtual std::vector getNbSteps() const = 0; + [[nodiscard]] virtual executor::DecodingMode getDecodingMode() const = 0; + //! @brief Initialize batched decoder at seqSlots with a new `requests`. virtual void newRequests(std::vector const& seqSlots, std::vector const& requests, std::vector const& samplingConfigs) diff --git a/cpp/include/tensorrt_llm/runtime/modelConfig.h b/cpp/include/tensorrt_llm/runtime/modelConfig.h index fd675075c..67be816a9 100644 --- a/cpp/include/tensorrt_llm/runtime/modelConfig.h +++ b/cpp/include/tensorrt_llm/runtime/modelConfig.h @@ -86,7 +86,6 @@ class ModelConfig , mModelVariant(ModelVariant::kGpt) , mUseCustomAllReduce(false) , mMaxPromptEmbeddingTableSize(0) - , mMaxDraftLen(0) , mContextFMHA(false) , mPagedContextFMHA(false) , mUseXQA{false} @@ -118,13 +117,11 @@ class ModelConfig [[nodiscard]] SizeType32 constexpr getNbAttentionLayers(SizeType32 pipelineParallelism = 1) const { - TLLM_CHECK(mNbAttentionLayers % pipelineParallelism == 0); return mNbAttentionLayers / pipelineParallelism; } [[nodiscard]] SizeType32 constexpr getNbRnnLayers(SizeType32 pipelineParallelism = 1) const { - TLLM_CHECK(mNbRnnLayers % pipelineParallelism == 0); return mNbRnnLayers / pipelineParallelism; } @@ -364,19 +361,14 @@ class ModelConfig mUseCustomAllReduce = customAllReduce; } - void constexpr setMaxDraftLen(SizeType32 maxDraftLen) noexcept + [[nodiscard]] SizeType32 getMaxDecodingDraftTokens() const { - mMaxDraftLen = maxDraftLen; + return getSpeculativeDecodingMode().isNone() ? 0 : getSpeculativeDecodingModule().getMaxDecodingDraftTokens(); } - [[nodiscard]] SizeType32 getMaxDraftLen() const + [[nodiscard]] SizeType32 constexpr getMaxDecodingTokens() const noexcept { - return mMaxDraftLen; - } - - [[nodiscard]] SizeType32 constexpr getMaxTokensPerStep() const noexcept - { - return mMaxDraftLen + 1; + return getSpeculativeDecodingMode().isNone() ? 1 : getSpeculativeDecodingModule().getMaxDecodingTokens(); } void constexpr setContextFMHA(bool contextFMHA) noexcept @@ -565,7 +557,7 @@ class ModelConfig mLayerTypes = layerTypes; } - [[nodiscard]] SpeculativeDecodingMode getSpeculativeDecodingMode() const noexcept + [[nodiscard]] SpeculativeDecodingMode constexpr getSpeculativeDecodingMode() const noexcept { return mSpeculativeDecodingMode; } @@ -618,8 +610,6 @@ class ModelConfig bool mUseCustomAllReduce; SizeType32 mMaxPromptEmbeddingTableSize; - // TODO(rkobus): remove this from ModelConfig and use mSpeculativeDecodingModule - SizeType32 mMaxDraftLen; bool mContextFMHA; bool mPagedContextFMHA; diff --git a/cpp/micro_benchmarks/README.md b/cpp/micro_benchmarks/README.md index 06896d8ee..39fc5e102 100644 --- a/cpp/micro_benchmarks/README.md +++ b/cpp/micro_benchmarks/README.md @@ -23,7 +23,7 @@ Usage: # or -./mixtureOfExpertsBackendBenchmark --benchmark_file +./mixtureOfExpertsBackendBenchmark --input_file ``` For more information see: diff --git a/cpp/micro_benchmarks/gen-moe-benchmark-file.py b/cpp/micro_benchmarks/gen-moe-benchmark-file.py index db6340c7a..deb3db8ef 100644 --- a/cpp/micro_benchmarks/gen-moe-benchmark-file.py +++ b/cpp/micro_benchmarks/gen-moe-benchmark-file.py @@ -27,13 +27,14 @@ def make_dtype_string(dtypes=None): return f'"dtypes": ["{join_term.join(dtypes)}"],' -def make_routing_string(name=None, values=None): +def make_routing_string(name=None, values=None, is_distribution=False): if values is None and name is None: return "" + values_field = "routing_distribution" if is_distribution else "routing_values" if values is None: - return f'"routing_values": "{name}",' + return f'"{values_field}": "{name}",' - values = f'"routing_values": [{",".join(values)}],' + values = f'"{values_field}": [{",".join(map(str, values))}],' if name is not None: values += f' "routing_values_name": "{name}",' diff --git a/cpp/micro_benchmarks/mixtureOfExpertsBackendBenchmarkFixture.h b/cpp/micro_benchmarks/mixtureOfExpertsBackendBenchmarkFixture.h index 9a278a806..35e0cd4e7 100644 --- a/cpp/micro_benchmarks/mixtureOfExpertsBackendBenchmarkFixture.h +++ b/cpp/micro_benchmarks/mixtureOfExpertsBackendBenchmarkFixture.h @@ -22,6 +22,7 @@ #include "tensorrt_llm/common/cudaUtils.h" #include "tensorrt_llm/common/memoryUtils.h" +#include "tensorrt_llm/common/nvtxUtils.h" #include "tensorrt_llm/kernels/cutlass_kernels/cutlass_preprocessors.h" #include "tensorrt_llm/kernels/mixtureOfExperts/moe_kernels.h" #include "tensorrt_llm/runtime/bufferManager.h" @@ -55,8 +56,13 @@ struct RoutingConfig { virtual void setRouting(float* routing_output, int64_t num_experts, int64_t k, int64_t num_tokens) = 0; virtual std::string getName() = 0; + virtual bool isDeterministic() const = 0; + virtual bool supportsConfig(int64_t num_experts, int64_t k, int64_t num_tokens) const = 0; }; +/** + * Generates a perfectly balanced routing configuration + */ struct LoadBalancedRoutingConfig : public RoutingConfig { std::string getName() override @@ -64,14 +70,141 @@ struct LoadBalancedRoutingConfig : public RoutingConfig return "balanced"; } + bool isDeterministic() const override + { + return true; + } + void setRouting(float* routing_output, int64_t num_experts, int64_t k, int64_t num_tokens) override { nvinfer1::DataType type = nvinfer1::DataType::kFLOAT; makeLoadBalancedRoutingConfiguration(routing_output, num_experts, num_tokens, k, type, streamPtr->get()); check_cuda_error(cudaStreamSynchronize(streamPtr->get())); } + + bool supportsConfig(int64_t, int64_t, int64_t) const override + { + return true; + } }; +/** + * Selects experts according to given random distribution + */ +struct RandomDistributionRoutingConfig : public RoutingConfig +{ + std::mt19937_64 twister{0xD5}; + std::vector probabilities; + std::pair shape; + std::string name; + + RandomDistributionRoutingConfig(std::vector const& in_probabilities, std::pair shape, + std::string name = "random_distribution") + : probabilities(std::move(in_probabilities)) + , shape(std::move(shape)) + , name(std::move(name)) + { + TLLM_CHECK_WITH_INFO(shape.second == probabilities.size(), + "Cannot create random routing distribution. Number of experts does not match the number of weights"); + } + + std::string getName() override + { + return name; + } + + bool isDeterministic() const override + { + return false; + } + + void doSample(float& curr_max, std::vector& selected) + { + std::uniform_real_distribution dist(0, curr_max); + float value = dist(twister); + float running_sum = 0; + for (int expert = 0; expert < probabilities.size(); expert++) + { + if (std::find(selected.begin(), selected.end(), expert) != selected.end()) + continue; // Already picked + float prob = probabilities[expert]; + running_sum += prob; + if (value < running_sum) + { + curr_max -= prob; + selected.push_back(expert); + return; + } + } + } + + void setRouting(float* routing_output, int64_t num_experts, int64_t k, int64_t num_tokens) override + { + TLLM_CHECK(num_experts == probabilities.size()); + std::vector input(num_experts * num_tokens, 0); + std::vector selected; + float max = std::accumulate(probabilities.begin(), probabilities.end(), 0.0f); + // TODO Put this on the GPU + for (int token = 0; token < num_tokens; token++) + { + selected.clear(); + float curr_max = max; + for (int choice = 0; choice < k; choice++) + { + doSample(curr_max, selected); + } + for (auto selection : selected) + { + input[token * num_experts + selection] = 1.f; + } + } + check_cuda_error(cudaMemcpyAsync( + routing_output, input.data(), input.size() * sizeof(float), cudaMemcpyHostToDevice, streamPtr->get())); + check_cuda_error(cudaStreamSynchronize(streamPtr->get())); + } + + bool supportsConfig(int64_t num_experts, int64_t, int64_t) const override + { + return num_experts == shape.second; + } +}; + +/** + * Generates routing values by sampling a uniform distribution [-1,1) + */ +struct UniformRoutingConfig : public RoutingConfig +{ + std::mt19937_64 twister{0xD5}; + + std::string getName() override + { + return "uniform"; + } + + bool isDeterministic() const override + { + return false; + } + + void setRouting(float* routing_output, int64_t num_experts, int64_t k, int64_t num_tokens) override + { + std::uniform_real_distribution dist(-1, 1); + std::vector input(num_experts * num_tokens); + std::generate(input.begin(), input.end(), [&] { return dist(twister); }); + check_cuda_error(cudaMemcpyAsync( + routing_output, input.data(), input.size() * sizeof(float), cudaMemcpyHostToDevice, streamPtr->get())); + check_cuda_error(cudaStreamSynchronize(streamPtr->get())); + } + + bool supportsConfig(int64_t, int64_t, int64_t) const override + { + return true; + } +}; + +/** + * Stores a specific routing configuration + */ struct VectoredRoutingConfig : public RoutingConfig { std::vector config; @@ -90,6 +223,11 @@ struct VectoredRoutingConfig : public RoutingConfig return name; } + bool isDeterministic() const override + { + return true; + } + void setRouting(float* routing_output, int64_t num_experts, int64_t k, int64_t num_tokens) override { assert(shape.second == num_experts); @@ -101,13 +239,21 @@ struct VectoredRoutingConfig : public RoutingConfig } check_cuda_error(cudaStreamSynchronize(streamPtr->get())); } + + bool supportsConfig(int64_t num_experts, int64_t, int64_t) const override + { + return shape.second == num_experts; + } }; }; // namespace constexpr int LOAD_BALANCED_ROUTING_CONFIG = 0; +constexpr int UNIFORM_ROUTING_CONFIG = 1; std::vector> routingConfigCache{ - std::static_pointer_cast(std::make_shared())}; + std::static_pointer_cast(std::make_shared()), + std::static_pointer_cast(std::make_shared()), +}; #ifdef ENABLE_FP8 using SafeFP8 = __nv_fp8_e4m3; @@ -222,6 +368,8 @@ class MixtureOfExpertsBenchmark : public ::benchmark::Fixture int64_t mInterSize{}; int64_t mTotalTokens{}; + int mRoutingConfigIndex = 0; + bool mUseBias = true; bool mIsGated = false; @@ -246,7 +394,7 @@ class MixtureOfExpertsBenchmark : public ::benchmark::Fixture } void initBuffersPermute(int64_t num_tokens, int64_t hidden_size, int64_t inter_size, int64_t num_experts, int64_t k, - int64_t routing_config) + int64_t routing_config, MOEParallelismConfig parallelism_config) { assert(hidden_size % BASE_HIDDEN_SIZE == 0); @@ -254,7 +402,7 @@ class MixtureOfExpertsBenchmark : public ::benchmark::Fixture mTotalTokens = num_tokens; mHiddenSize = hidden_size; - mInterSize = inter_size; + mInterSize = inter_size / parallelism_config.tp_size; mNumExperts = num_experts; mK = k; mIsGated = tensorrt_llm::isGatedActivation(mActType); @@ -303,6 +451,7 @@ class MixtureOfExpertsBenchmark : public ::benchmark::Fixture mSourceToExpandedMap = allocBuffer(mTotalTokens * mK); mSelectedExpert = allocBuffer(mTotalTokens * mK); + mRoutingConfigIndex = routing_config; auto tactic = routingConfigCache.at(routing_config); tactic->setRouting(mInputProbabilities, mNumExperts, mK, mTotalTokens); @@ -311,10 +460,19 @@ class MixtureOfExpertsBenchmark : public ::benchmark::Fixture float benchmarkLoop(MOEParallelismConfig parallelism_config) { - check_cuda_error(cudaEventRecord(mStartEvent, streamPtr->get())); - runMoEPermute(parallelism_config); - check_cuda_error(cudaEventRecord(mEndEvent, streamPtr->get())); - check_cuda_error(cudaStreamSynchronize(streamPtr->get())); + auto tactic = routingConfigCache.at(mRoutingConfigIndex); + if (!tactic->isDeterministic()) + { + tactic->setRouting(mInputProbabilities, mNumExperts, mK, mTotalTokens); + } + + { + NVTX3_SCOPED_RANGE(BenchmarkLoopIteration); + check_cuda_error(cudaEventRecord(mStartEvent, streamPtr->get())); + runMoEPermute(parallelism_config); + check_cuda_error(cudaEventRecord(mEndEvent, streamPtr->get())); + check_cuda_error(cudaStreamSynchronize(streamPtr->get())); + } float ms; check_cuda_error(cudaEventElapsedTime(&ms, mStartEvent, mEndEvent)); @@ -325,6 +483,7 @@ class MixtureOfExpertsBenchmark : public ::benchmark::Fixture // Runs for 3 iterations or 1 second and picks the best option int pickBestTactic(MOEParallelismConfig parallelism_config) { + NVTX3_SCOPED_RANGE(WarmUpRun); auto tactics = mMoERunner.getTactics(); float best_time = INFINITY; @@ -411,6 +570,7 @@ class MixtureOfExpertsBenchmark : public ::benchmark::Fixture template void MixtureOfExpertsBenchmark::runBenchmark(benchmark::State& state) { + NVTX3_SCOPED_RANGE(FullBenchmark); int const num_experts = state.range(0); int const top_k = state.range(1); int const hidden_size = state.range(2); @@ -448,10 +608,11 @@ void MixtureOfExpertsBenchmark::runBenchmark(benchmark::State& state } ss << routingConfigCache.at(routing_config)->getName(); // state.SetLabel(ss.str()); + state.SetLabel(routingConfigCache.at(routing_config)->getName()); // Always use EP size for moe config until we support TP+EP, we just divide the inter size for TP - MOEParallelismConfig parallelism_config = MOEParallelismConfig::ExpertParallelism(ep_size, world_rank / tp_size); - initBuffersPermute(num_tokens, hidden_size, inter_size / tp_size, num_experts, top_k, routing_config); + MOEParallelismConfig parallelism_config{tp_size, world_rank / ep_size, ep_size, world_rank % ep_size}; + initBuffersPermute(num_tokens, hidden_size, inter_size, num_experts, top_k, routing_config, parallelism_config); // Parse the tactic, does checks for "auto" mode and out of range tactic_idx = setTactic(tactic_idx, parallelism_config); @@ -468,10 +629,13 @@ void MixtureOfExpertsBenchmark::runBenchmark(benchmark::State& state } state.counters["tactic_idx"] = tactic_idx; - for (auto _ : state) { - float ms = benchmarkLoop(parallelism_config); - state.SetIterationTime(ms / 1000.f); + NVTX3_SCOPED_RANGE(BenchmarkRun); + for (auto _ : state) + { + float ms = benchmarkLoop(parallelism_config); + state.SetIterationTime(ms / 1000.f); + } } state.SetItemsProcessed(state.iterations() * num_tokens); diff --git a/cpp/micro_benchmarks/mixtureOfExpertsBackendBenchmarkLauncher.cu b/cpp/micro_benchmarks/mixtureOfExpertsBackendBenchmarkLauncher.cu index 54c420dbb..3eb350dd2 100644 --- a/cpp/micro_benchmarks/mixtureOfExpertsBackendBenchmarkLauncher.cu +++ b/cpp/micro_benchmarks/mixtureOfExpertsBackendBenchmarkLauncher.cu @@ -224,6 +224,7 @@ void parseTacticToVectorID(nlohmann::json& tactic, std::vector& tactic_ids) // This interdependence of globals could be better, but it works ok for this limited case. std::unordered_map> name_info_map{ {routingConfigCache[LOAD_BALANCED_ROUTING_CONFIG]->getName(), {-1, LOAD_BALANCED_ROUTING_CONFIG}}, + {routingConfigCache[UNIFORM_ROUTING_CONFIG]->getName(), {-1, UNIFORM_ROUTING_CONFIG}}, }; int getNameCacheIdx(std::string const& name) @@ -240,82 +241,58 @@ void setNameCacheIdx(std::string const& name, int id) name_info_map.at(name).second = id; } +template +std::optional loadRoutingValues(nlohmann::json entry, int64_t num_experts, std::string config_name) +{ + std::optional routing_config; + if (entry.is_string()) + { + routing_config = getNameCacheIdx(entry.get()); + if (routing_config < 0) + { + throw std::invalid_argument("Invalid routing value, could not find valid config"); + } + } + else + { + if (config_name.empty()) + { + throw std::invalid_argument("Explicit routing configurations must specify a name"); + } + std::vector routing_values; + entry.get_to(routing_values); + + int64_t shape = routing_values.size() / num_experts; + routingConfigCache.push_back(std::make_shared( + std::move(routing_values), std::pair{shape, num_experts}, config_name)); + routing_config = routingConfigCache.size() - 1; + } + + auto conf = routingConfigCache[*routing_config]; + + bool const is_supported = conf->supportsConfig(num_experts, {}, {}); + auto conf_derived = std::dynamic_pointer_cast(conf); + auto conf_default = std::dynamic_pointer_cast, + LoadBalancedRoutingConfig, UniformRoutingConfig>>(conf); + bool const is_valid_type = conf_derived || conf_default; + + if (!is_supported || !is_valid_type) + { + throw std::invalid_argument("Incompatible config selected. " + + ((conf_derived) ? "Expected " + std::to_string(num_experts) + + " experts in routing configuration. Found: " + std::to_string(conf_derived->shape.second) + : "Found incompatible routing config type")); + } + + return routing_config; +} + // This is suboptimal for large benchmark files as we reread it for every data type template void argGenLoadFile(benchmark::internal::Benchmark* benchmark) { /* - * File schema - * - * [ - * { - * "num_experts": int, - * "k": int, - * "hidden_size": int, - * "inter_size": int, - * "tp_size": int, (optional) - * "ep_size": int, (optional) - * "world_rank": int, (optional) - * "num_tokens": int, - * "bias": int, - * "act_fn": int, - * "norm_mode": int, - * "tactic_id": tactic, (see below) - * "dtypes": [string, ...], (optional) - * "routing_values_name": string, (optional) - * "routing_values": [float, ...], or string, (optional, length is a multiple of num_experts) - * }, - * ... - * ] - * - * Explanation: - * - * - "num_experts" - The number of experts - * - "k" - The top k - * - "hidden_size" - The hidden size - * - "inter_size" - The inter size - * - "tp_size" - The TP size - * - "ep_size" - The EP size - * - "world_rank" - The world rank = ep_rank * tp_size + tp_rank - * - "num_tokens" - The total number of tokens to benchmark - * - "bias" - If bias should be used, 0 = no bias, 1 = bias - * - "act_fn" - The enum value of the activation function. See - * "cpp/tensorrt_llm/kernels/cutlass_kernels/moe_gemm/moe_gemm_kernels.h" - * - "norm_mode" - The normalization mode. 0 = NONE, 1 = RENORM. See - * "cpp/tensorrt_llm/kernels/cutlass_kernels/moe_gemm/moe_gemm_kernels.h" - * - * - "tactic_id" - * Valid tactics are: - * - An object: - * { - * "is_sm90": bool, - * "tile_shape": [int, int, int] or int, - * "cluster_shape": [int, int, int] or int, (required for sm90, type must be an int if tile_shape is an int) - * "warp_shape": [int, int, int], (required for non-sm90 if tile_shape is an array) - * "stages": int, (required for non-sm90) - * }, - * - An integer: corresponds to an index in the tactics array. WARNING this is not stable between test - * configurations - * - An array: of integers or objects, forms a list of tactics to sweep - * - The string "all": This will sweep through all possible tactics - * - The string "auto": This runs a short benchmark to pick the fastest tactic before each benchmark case. Useful - * for quick perf tests, prefer a full sweep and manually setting the tactic for more accurate results - * - * - dtypes - A list of dtypes to run this config through. - * Allowed values are: fp8, int4, int8, float, half, bfloat16 - * If this argument is omitted all dtypes will be run. Note, not all tactics are supported for all dtypes, - * unsupported tactics will be skipped with a warning. - * - * - "routing_values_name" - a name to help identify the routing pattern. This can be used by later configs to reuse - * the config - * - "routing_values" - a flat array of routing values to define a new config, or a string referencing the name of a - * previous config. Defaults to "balanced", which is short-hand for a uniform expert distribution - * These define the routing values used as input to the moe backend, and is intended to allow comparing different - * routing behaviours. - * When defining an array, it must have `T*num_experts` floating point values. Each set of - * `num_experts` values defines the input for a single token. If `num_tokens` is greater than `T` it will repeat - * from the beginning - * + * See help text for schema description */ std::ifstream file{workloadFile}; @@ -338,7 +315,7 @@ void argGenLoadFile(benchmark::internal::Benchmark* benchmark) if (run_config.contains("routing_values_name")) { run_config["routing_values_name"].get_to(config_name); - if (!run_config.contains("routing_values")) + if (!run_config.contains("routing_values") && !run_config.contains("routing_distribution")) { throw std::invalid_argument("Setting routing value configuration name but missing routing values"); } @@ -361,39 +338,17 @@ void argGenLoadFile(benchmark::internal::Benchmark* benchmark) int num_experts = run_config.at("num_experts").get(); - if (run_config.contains("routing_values") && !routing_config) + if (!routing_config) { - if (run_config["routing_values"].is_string()) + if (run_config.contains("routing_values")) { - routing_config = getNameCacheIdx(run_config["routing_values"].get()); - if (routing_config < 0) - { - throw std::invalid_argument("Invalid routing value, could not find valid config"); - } + routing_config + = loadRoutingValues(run_config["routing_values"], num_experts, config_name); } - else + else if (run_config.contains("routing_distribution")) { - if (config_name.empty()) - { - throw std::invalid_argument("Explicit routing configurations must specify a name"); - } - std::vector routing_values; - run_config["routing_values"].get_to(routing_values); - - int shape = routing_values.size() / num_experts; - routingConfigCache.push_back(std::make_shared( - std::move(routing_values), std::pair(shape, num_experts), config_name)); - routing_config = routingConfigCache.size() - 1; - } - - auto conf = routingConfigCache[*routing_config]; - auto conf_vec = std::dynamic_pointer_cast(conf); - if (conf->getName() != "balanced" && (!conf_vec || conf_vec->shape.second != num_experts)) - { - throw std::invalid_argument("Incompatible config selected. Expected " + std::to_string(num_experts) - + " experts in routing configuration. " - + ((conf_vec) ? "Found: " + std::to_string(conf_vec->shape.second) - : "Found incompatible routing config type")); + routing_config = loadRoutingValues( + run_config["routing_distribution"], num_experts, config_name); } } // Use the selected config or fall back to balanced @@ -462,6 +417,7 @@ void argGenLoadFile(benchmark::internal::Benchmark* benchmark) int ep_size = get_or("ep_size", 1); int world_rank = get_or("world_rank", 0); int bias = get_or("bias", 0); + TLLM_CHECK_WITH_INFO(world_rank < tp_size * ep_size, "Rank is out of bounds of tp*ep"); for (auto tactic_id : tactic_ids) { @@ -483,61 +439,18 @@ void argGenLoadFile(benchmark::internal::Benchmark* benchmark) template void argGenHardcoded(benchmark::internal::Benchmark* benchmark) { - auto num_tactics = listAllTactics().size(); - for (auto [tp, ep] : std::vector>{{8, 1}, {1, 8}, {2, 4}, {4, 2}}) - { - for (int i = 0; i < num_tactics; i++) - { - for (auto tokens : {1, 64, 2048}) - { - benchmark->Args({ - 16, // Experts - 2, // K - 15360, // hidden - 30720, // inter - tp, // TP Size - ep, // EP Size - 0, // World Rank - tokens, // Num tokens - 0, // bias - (int) tensorrt_llm::ActivationType::Gelu, // Act fn - (int) MOEExpertScaleNormalizationMode::RENORMALIZE, // Norm mode - i, // Tactic ID. Index into getTactics() function result, see argGenLoadFile() for examples - LOAD_BALANCED_ROUTING_CONFIG // Routing configuration id - }); - - benchmark->Args({ - 16, // Experts - 2, // K - 15360, // hidden - 20480, // inter - tp, // TP Size - ep, // EP Size - 0, // World Rank - tokens, // Num tokens - 0, // bias - (int) tensorrt_llm::ActivationType::Swiglu, // Act fn - (int) MOEExpertScaleNormalizationMode::RENORMALIZE, // Norm mode - i, // Tactic ID. Index into getTactics() function result, see argGenLoadFile() for examples - LOAD_BALANCED_ROUTING_CONFIG // Routing configuration id - }); - } - } - } - - return; auto num_experts = {1, 8, 9, 64, 65, 257}; // {1, 8, 64, 65, 1024}; auto top_k = {1, 2, 3, 16}; // {1, 2, 3, 42}; - auto hidden_size = {32}; // {8, 32, 96, 256, 1024}; + auto hidden_size = {4096}; auto inter_size_mul = {4.f}; // {7.f/2.f, 4.f}; - auto num_tokens = {200}; // {1, 20, 200}; - auto use_bias = {0, 1}; // {0, 1}; + auto num_tokens = {2048}; // {1, 20, 200, 2048}; + auto use_bias = {0}; // {0, 1}; auto activation_type = {tensorrt_llm::ActivationType::Gelu}; // {tensorrt_llm::ActivationType::Relu, tensorrt_llm::ActivationType::Gelu, // tensorrt_llm::ActivationType::Silu, tensorrt_llm::ActivationType::Geglu, // tensorrt_llm::ActivationType::Swiglu}; auto norm_mode = {MOEExpertScaleNormalizationMode::NONE}; - auto cutlass_tactic = {0}; // {0, 1, 2}; + auto cutlass_tactic = {-1}; // {0,..., listAllTactics().size()}; auto routing_config = {LOAD_BALANCED_ROUTING_CONFIG}; // {0, 1, 2}; for (auto num_expert : num_experts) @@ -645,6 +558,7 @@ void help() " \"dtypes\": [string, ...], (optional)\n" " \"routing_values_name\": string, (optional)\n" " \"routing_values\": [float, ...], or string, (optional, length is a multiple of num_experts)\n" + " \"routing_distribution\": [float, ...], or string, (optional, length is num_experts)\n" " },\n" " ...\n" "]\n" @@ -655,7 +569,7 @@ void help() "- \"inter_size\" - The inter size\n" "- \"tp_size\" - The TP size to use\n" "- \"ep_size\" - The EP size to use\n" - "- \"world_rank\" - The world rank = ep_rank * tp_size + tp_rank\n" + "- \"world_rank\" - The world rank = tp_rank * ep_size + ep_rank\n" "- \"num_tokens\" - The total number of tokens to benchmark\n" "- \"bias\" - If bias should be used, 0 = no bias, 1 = bias\n" "- \"act_fn\" - The enum value of the activation function. See\n" @@ -687,12 +601,17 @@ void help() "benchmarks to reuse the config\n" "- \"routing_values\" - a flat array of routing values to define a new config, or a string referencing " "the name of a\n" - "previous config. Defaults to \"balanced\", which is short-hand for a uniform expert distribution\n" + "previous config. Defaults to pre-defined config \"balanced\", which is short-hand for a perfectly balanced " + "expert distribution\n" "These define the routing values used as input to the moe backend, and is intended to allow comparing " "different routing behaviours.\n" "When defining an array, it must have `T*num_experts` floating point values. Each set of\n" "`num_experts` values defines the input for a single token. If `num_tokens` is greater than `T` it will " - "repeat from the beginning\n\n"; + "repeat from the beginning\n" + "- \"routing_distribution\" - instead of explicitly setting routing_values, define a random distribution " + "that experts will be randomly sampled from." + "There is also pre-defined config \"uniform\", which is short-hand for a random uniform distribution\n" + "\n"; std::cout << "benchmark options:\n"; benchmark::PrintDefaultHelp(); diff --git a/cpp/tensorrt_llm/batch_manager/aarch64-linux-gnu/libtensorrt_llm_batch_manager_static.a b/cpp/tensorrt_llm/batch_manager/aarch64-linux-gnu/libtensorrt_llm_batch_manager_static.a index fe7949a5b..c6e8dad75 100644 --- a/cpp/tensorrt_llm/batch_manager/aarch64-linux-gnu/libtensorrt_llm_batch_manager_static.a +++ b/cpp/tensorrt_llm/batch_manager/aarch64-linux-gnu/libtensorrt_llm_batch_manager_static.a @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:8b6ad33047e2684c7d22471f87febbb96ae26f4eac6529e2f3b7c1469ec2ec6d -size 3931504 +oid sha256:33f2d6b3e871b0a0e651883607887777fe03d6822f06e4154ffc7e35a8d5cc70 +size 3938416 diff --git a/cpp/tensorrt_llm/batch_manager/aarch64-linux-gnu/libtensorrt_llm_batch_manager_static.pre_cxx11.a b/cpp/tensorrt_llm/batch_manager/aarch64-linux-gnu/libtensorrt_llm_batch_manager_static.pre_cxx11.a index 8d15a4cc4..731820756 100644 --- a/cpp/tensorrt_llm/batch_manager/aarch64-linux-gnu/libtensorrt_llm_batch_manager_static.pre_cxx11.a +++ b/cpp/tensorrt_llm/batch_manager/aarch64-linux-gnu/libtensorrt_llm_batch_manager_static.pre_cxx11.a @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:560f736af15a4dfba849ab29efc3520d6ec8c87bf2aa16589299b232dc171cca -size 3989220 +oid sha256:8412aa4ca15c232ced1cd4bdfcc54177c7b257aef493d50650c960e0fb527cfc +size 4002178 diff --git a/cpp/tensorrt_llm/batch_manager/aarch64-linux-gnu/version.txt b/cpp/tensorrt_llm/batch_manager/aarch64-linux-gnu/version.txt index d1e552ba5..979f9c627 100644 --- a/cpp/tensorrt_llm/batch_manager/aarch64-linux-gnu/version.txt +++ b/cpp/tensorrt_llm/batch_manager/aarch64-linux-gnu/version.txt @@ -1,3 +1,3 @@ -f8538ac35803837e5d457ea8c1a58053 libtensorrt_llm_batch_manager_static.a -dc6fc82dc4ba319899e1d6777bd8c3a4 libtensorrt_llm_batch_manager_static.pre_cxx11.a -265b039443334094026fbd8f396d52fe29c2d9d1 commit \ No newline at end of file +d4aa7db860caf8feedb79a280aa70da3 libtensorrt_llm_batch_manager_static.a +02b4363342ccea3e2abccc474f3506bb libtensorrt_llm_batch_manager_static.pre_cxx11.a +0e1417f27d93de67940c1062cf230017cd8be5f1 commit \ No newline at end of file diff --git a/cpp/tensorrt_llm/batch_manager/x86_64-linux-gnu/libtensorrt_llm_batch_manager_static.a b/cpp/tensorrt_llm/batch_manager/x86_64-linux-gnu/libtensorrt_llm_batch_manager_static.a index 09e9e4105..5b09fa994 100644 --- a/cpp/tensorrt_llm/batch_manager/x86_64-linux-gnu/libtensorrt_llm_batch_manager_static.a +++ b/cpp/tensorrt_llm/batch_manager/x86_64-linux-gnu/libtensorrt_llm_batch_manager_static.a @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:74948e00ff7341914b1831ccfdce9ae242dd149603b1ba7e24ee993f08b63542 -size 3812960 +oid sha256:86f34c84883f1dfed04c6fb18811198da636e4457617a47db71f045cb3066eb4 +size 3825822 diff --git a/cpp/tensorrt_llm/batch_manager/x86_64-linux-gnu/libtensorrt_llm_batch_manager_static.pre_cxx11.a b/cpp/tensorrt_llm/batch_manager/x86_64-linux-gnu/libtensorrt_llm_batch_manager_static.pre_cxx11.a index effd33642..a151b44cf 100644 --- a/cpp/tensorrt_llm/batch_manager/x86_64-linux-gnu/libtensorrt_llm_batch_manager_static.pre_cxx11.a +++ b/cpp/tensorrt_llm/batch_manager/x86_64-linux-gnu/libtensorrt_llm_batch_manager_static.pre_cxx11.a @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:0421ceacd5d07bc172bb4d0979edaf466aa8950290b4d6d1a7d355dbcefc2c84 -size 3772832 +oid sha256:c07c30d986591bbe93bb30d67fc8ebbba3eb55c5875ce939c3265151747656ae +size 3782506 diff --git a/cpp/tensorrt_llm/batch_manager/x86_64-windows-msvc/tensorrt_llm_batch_manager_static.lib b/cpp/tensorrt_llm/batch_manager/x86_64-windows-msvc/tensorrt_llm_batch_manager_static.lib index 04b00e8b8..81d6c151f 100644 --- a/cpp/tensorrt_llm/batch_manager/x86_64-windows-msvc/tensorrt_llm_batch_manager_static.lib +++ b/cpp/tensorrt_llm/batch_manager/x86_64-windows-msvc/tensorrt_llm_batch_manager_static.lib @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:46eb1d351e3e8da3945a3f451166f12536aae3e440d57337d8891492424aff78 -size 22387798 +oid sha256:e0190b794e437fa6a0e2140e9446195413abde0dfbc5109423c790397fbb95a6 +size 22445474 diff --git a/cpp/tensorrt_llm/common/envUtils.cpp b/cpp/tensorrt_llm/common/envUtils.cpp index f38c71f43..a7d7102ec 100644 --- a/cpp/tensorrt_llm/common/envUtils.cpp +++ b/cpp/tensorrt_llm/common/envUtils.cpp @@ -55,9 +55,10 @@ std::optional envXqaNbCtaPerKVHead() return ret; } -bool getEnvEnableXQAJIT() +std::optional getEnvEnableXQAJIT() { static bool init = false; + static bool exists = false; static bool enableXQAJIT = false; if (!init) { @@ -65,13 +66,21 @@ bool getEnvEnableXQAJIT() char const* enable_xqa_jit_var = std::getenv("TRTLLM_ENABLE_XQA_JIT"); if (enable_xqa_jit_var) { + exists = true; if (enable_xqa_jit_var[0] == '1' && enable_xqa_jit_var[1] == '\0') { enableXQAJIT = true; } } } - return enableXQAJIT; + if (exists) + { + return enableXQAJIT; + } + else + { + return std::nullopt; + } } // Tune the number of blocks per sequence for accuracy/performance purpose. diff --git a/cpp/tensorrt_llm/common/envUtils.h b/cpp/tensorrt_llm/common/envUtils.h index 2bafc149c..73b908694 100644 --- a/cpp/tensorrt_llm/common/envUtils.h +++ b/cpp/tensorrt_llm/common/envUtils.h @@ -34,7 +34,9 @@ int32_t xqaMaxNbCtaPerKVHeadFactor(); std::optional envXqaNbCtaPerKVHead(); // Whether XQA JIT is enabled. -bool getEnvEnableXQAJIT(); +// +// Returns the value of TRTLLM_ENABLE_XQA_JIT env var. If such env var doesn't exist, std::nullopt is returned. +std::optional getEnvEnableXQAJIT(); // Tune the number of blocks per sequence for accuracy/performance purpose. bool getEnvMmhaMultiblockDebug(); diff --git a/cpp/tensorrt_llm/common/mpiUtils.cpp b/cpp/tensorrt_llm/common/mpiUtils.cpp index c3cb8aad9..8916960eb 100644 --- a/cpp/tensorrt_llm/common/mpiUtils.cpp +++ b/cpp/tensorrt_llm/common/mpiUtils.cpp @@ -91,13 +91,18 @@ namespace { bool mpiInitialized = false; -std::mutex mpiMutex; +std::recursive_mutex mpiMutex; } // namespace void initialize(MpiThreadSupport threadMode, bool forwardAbortToParent) { - std::lock_guard lk(mpiMutex); + // double-checked locking + if (mpiInitialized) + { + return; + } + std::lock_guard lk(mpiMutex); if (mpiInitialized) { return; @@ -293,13 +298,19 @@ int MpiComm::getSize() const MpiComm const& MpiComm::world() { + TLLM_LOG_TRACE("%s start", __PRETTY_FUNCTION__); static MpiComm commWorld{MPI_COMM_WORLD, false}; + initialize(); + TLLM_LOG_TRACE("%s stop", __PRETTY_FUNCTION__); return commWorld; } MpiComm& MpiComm::session() { - static MpiComm commSession{world(), false}; + TLLM_LOG_TRACE("%s start", __PRETTY_FUNCTION__); + static MpiComm commSession{MPI_COMM_WORLD, false}; + initialize(); + TLLM_LOG_TRACE("%s stop", __PRETTY_FUNCTION__); return commSession; } @@ -307,17 +318,19 @@ MpiComm getLocalSession() { #if ENABLE_MULTI_DEVICE MPI_Comm localComm; - MPI_Comm_split_type(MPI_COMM_WORLD, OMPI_COMM_TYPE_HOST, 0, MPI_INFO_NULL, &localComm); + MPI_Comm_split_type(COMM_SESSION, OMPI_COMM_TYPE_HOST, 0, MPI_INFO_NULL, &localComm); MpiComm localSession{localComm, false}; #else - MpiComm localSession{MPI_COMM_WORLD, false}; + MpiComm localSession{COMM_SESSION, false}; #endif // ENABLE_MULTI_DEVICE return localSession; } MpiComm& MpiComm::localSession() { + TLLM_LOG_TRACE("%s start", __PRETTY_FUNCTION__); static MpiComm localSession = getLocalSession(); + TLLM_LOG_TRACE("%s stop", __PRETTY_FUNCTION__); return localSession; } @@ -326,10 +339,6 @@ MpiComm::MpiComm(MPI_Comm g, bool freeComm) , mFreeComm{freeComm} { TLLM_CHECK(mComm != MPI_COMM_NULL); - if (g == MPI_COMM_WORLD) - { - initialize(); - } } MpiComm::~MpiComm() noexcept diff --git a/cpp/tensorrt_llm/executor/aarch64-linux-gnu/libtensorrt_llm_executor_static.a b/cpp/tensorrt_llm/executor/aarch64-linux-gnu/libtensorrt_llm_executor_static.a index 932e0f372..29476045d 100644 --- a/cpp/tensorrt_llm/executor/aarch64-linux-gnu/libtensorrt_llm_executor_static.a +++ b/cpp/tensorrt_llm/executor/aarch64-linux-gnu/libtensorrt_llm_executor_static.a @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:19585b7709736197d9c1762d1bb8e3099e298d6dcc1c521d51c83637cc624c20 -size 1397814 +oid sha256:8729077e2bfb9cf3f647cc6ca9be42a8953c0ddf58426485ae3bded76dc9d5c3 +size 1403008 diff --git a/cpp/tensorrt_llm/executor/aarch64-linux-gnu/libtensorrt_llm_executor_static.pre_cxx11.a b/cpp/tensorrt_llm/executor/aarch64-linux-gnu/libtensorrt_llm_executor_static.pre_cxx11.a index 926f354f0..871ecac8f 100644 --- a/cpp/tensorrt_llm/executor/aarch64-linux-gnu/libtensorrt_llm_executor_static.pre_cxx11.a +++ b/cpp/tensorrt_llm/executor/aarch64-linux-gnu/libtensorrt_llm_executor_static.pre_cxx11.a @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:f5d5475663640c068af2e9b5772b9b602656641dd17ca473ce7125ef7f2ec855 -size 1423172 +oid sha256:2b68c06565f1b3f795e070420c73d085c620b42c1c2131f9895d2687178a6b54 +size 1427780 diff --git a/cpp/tensorrt_llm/executor/aarch64-linux-gnu/version.txt b/cpp/tensorrt_llm/executor/aarch64-linux-gnu/version.txt index fce860082..6b4481d70 100644 --- a/cpp/tensorrt_llm/executor/aarch64-linux-gnu/version.txt +++ b/cpp/tensorrt_llm/executor/aarch64-linux-gnu/version.txt @@ -1,3 +1,3 @@ -e18e84fb356995b11c04b79e55c4c3f5 libtensorrt_llm_executor_static.a -f0555b76f21d43e676e5808bf197cc58 libtensorrt_llm_executor_static.pre_cxx11.a -265b039443334094026fbd8f396d52fe29c2d9d1 commit \ No newline at end of file +db98ffd911c3c1dde3413e934ce8deb8 libtensorrt_llm_executor_static.a +8dc57746aa2c29d8a2fa50196b552bc3 libtensorrt_llm_executor_static.pre_cxx11.a +0e1417f27d93de67940c1062cf230017cd8be5f1 commit \ No newline at end of file diff --git a/cpp/tensorrt_llm/executor/x86_64-linux-gnu/libtensorrt_llm_executor_static.a b/cpp/tensorrt_llm/executor/x86_64-linux-gnu/libtensorrt_llm_executor_static.a index 702aee286..af916f3ad 100644 --- a/cpp/tensorrt_llm/executor/x86_64-linux-gnu/libtensorrt_llm_executor_static.a +++ b/cpp/tensorrt_llm/executor/x86_64-linux-gnu/libtensorrt_llm_executor_static.a @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:8496c9e4a20efd3d2072520cf843dac70cbb0fe23621cfba2a1e0ef3e5fa22ed -size 1450288 +oid sha256:cbc3a279681e877a982c0ebbdd0c13d7792d67a87bad0be125ec81bfe3f87399 +size 1454684 diff --git a/cpp/tensorrt_llm/executor/x86_64-linux-gnu/libtensorrt_llm_executor_static.pre_cxx11.a b/cpp/tensorrt_llm/executor/x86_64-linux-gnu/libtensorrt_llm_executor_static.pre_cxx11.a index 24277fcfa..de53b7818 100644 --- a/cpp/tensorrt_llm/executor/x86_64-linux-gnu/libtensorrt_llm_executor_static.pre_cxx11.a +++ b/cpp/tensorrt_llm/executor/x86_64-linux-gnu/libtensorrt_llm_executor_static.pre_cxx11.a @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:1b76267834252836e26ddecc2e1b9449e33a67fb1981e5d42f721bc439be1c02 -size 1377018 +oid sha256:aa15303c38a748c4bf7b82e1f9c58cb63418efbd60bfede62820f5a62d65710a +size 1381738 diff --git a/cpp/tensorrt_llm/executor/x86_64-windows-msvc/tensorrt_llm_executor_static.lib b/cpp/tensorrt_llm/executor/x86_64-windows-msvc/tensorrt_llm_executor_static.lib index 05a1eccbc..58c55281e 100644 --- a/cpp/tensorrt_llm/executor/x86_64-windows-msvc/tensorrt_llm_executor_static.lib +++ b/cpp/tensorrt_llm/executor/x86_64-windows-msvc/tensorrt_llm_executor_static.lib @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:9bd0faf48175745d7aeff58f539ae021db365b73933dab9c51329de9e92f2d86 -size 14039826 +oid sha256:1a3e774d6700444b7164e1b31e26936ea6fcddc73e3e17bba1d8492c65a57b78 +size 14036486 diff --git a/cpp/tensorrt_llm/kernels/beamSearchKernels.h b/cpp/tensorrt_llm/kernels/beamSearchKernels.h index 8cfa8ae55..2faa3a1fa 100644 --- a/cpp/tensorrt_llm/kernels/beamSearchKernels.h +++ b/cpp/tensorrt_llm/kernels/beamSearchKernels.h @@ -16,6 +16,7 @@ #pragma once #include "tensorrt_llm/kernels/decodingCommon.h" +#include "tensorrt_llm/runtime/common.h" namespace tensorrt_llm { @@ -40,7 +41,6 @@ struct BeamHypotheses int nMaxBatchSize{0}; // max batch size by model configuration int nBatchSize{0}; // batch size by runtime input data int nBeamWidth{0}; // - int nIte{0}; // index of local_batch, always be 0 when pp_size==1 int nMaxSeqLen{0}; // int nVocabSize{0}; // vocab_size_padded @@ -52,6 +52,7 @@ struct BeamHypotheses // Pointers from input int const* inputLengths{nullptr}; // [BS, BM] %% context_length int const* endIds{nullptr}; // [BS, BM] %% self.end_ids + runtime::SizeType32 const* batchSlots{nullptr}; // [BS] // Pointers for output int* outputIds{nullptr}; // [BS, BM, MSL] %% self.output_ids only used in gather_tree diff --git a/cpp/tensorrt_llm/kernels/beamSearchKernels/beamSearchKernelsTemplate.h b/cpp/tensorrt_llm/kernels/beamSearchKernels/beamSearchKernelsTemplate.h index 75e20456f..dc104f8c5 100644 --- a/cpp/tensorrt_llm/kernels/beamSearchKernels/beamSearchKernelsTemplate.h +++ b/cpp/tensorrt_llm/kernels/beamSearchKernels/beamSearchKernelsTemplate.h @@ -36,8 +36,6 @@ namespace tensorrt_llm namespace kernels { -#define DO_SPLIT_SMALL_TOP_K_SOFTMAX - #define TOPK_FP16_STORAGE 0 #pragma nv_diag_suppress static_var_with_dynamic_init @@ -46,17 +44,17 @@ template __launch_bounds__(THREADBLOCK_SIZE) __global__ void beamStage3Kernel(int const* __restrict pTempId, T const* __restrict pTempVal, BeamHypotheses bh) { - int const bid = blockIdx.x; // Index of Batch + int const bid = blockIdx.x; // Index of Batch int const tid = threadIdx.x; + auto const slot = bh.batchSlots ? bh.batchSlots[bid] : bid; int const nMBS{bh.nMaxBatchSize}; // Only for bh.logProbsTiled - int const nBS{bh.nBatchSize}; - int const gbid{nBS * bh.nIte + bid}; // Global batch index, TODO: distinguish bid and gbid in the future int const nBM{bh.nBeamWidth}; int const nCandidate{nBM * nBM * 2}; // Keep top 2K candidates from each beam output int const nV{bh.nVocabSize}; - float const diversityRate{bh.diversityRates[gbid]}; - float const lengthPenalty{bh.lengthPenalties[gbid]}; - int const earlyStopping{bh.earlyStoppings[gbid]}; + float const diversityRate{bh.diversityRates[slot]}; + float const lengthPenalty{bh.lengthPenalties[slot]}; + int const earlyStopping{bh.earlyStoppings[slot]}; + T const MAX_T_VAL = std::is_same_v ? HALF_FLT_MAX : FLT_MAX; __shared__ int nBeamForNextStep; // Only used by thread of tid == 0 @@ -68,26 +66,26 @@ __launch_bounds__(THREADBLOCK_SIZE) __global__ } if (tid < nBM) { - smemCumLogProbs[tid] = bh.cumLogProbs[bid * nBM + tid]; + smemCumLogProbs[tid] = bh.cumLogProbs[slot * nBM + tid]; } __syncthreads(); if (bh.numBeamsCBA != nullptr) { // Beam search is enabled - if (bh.numBeamsCBA[gbid] == 0 && tid == 0) + if (bh.numBeamsCBA[slot] == 0 && tid == 0) { // Initialize worst score in the first call - bh.minNormedScoresCBA[gbid] = FLT_MAX; + bh.minNormedScoresCBA[slot] = FLT_MAX; } - else if (earlyStopping == 1 && bh.numBeamsCBA[gbid] == nBM - || earlyStopping != 1 && bh.finished[bid * nBM].isFinished()) + else if (earlyStopping == 1 && bh.numBeamsCBA[slot] == nBM + || earlyStopping != 1 && bh.finished[slot * nBM].isFinished()) { // Condition of early return: // 1. In EarlyStopping mode, and we have got enough beams // 2. In NonEarlyStopping mode, and this batch has been marked as done // TODO: improve the condition like below - // earlyStopping == 1 && bh.numBeamsCBA[gbid] == nBM || earlyStopping != 1 && bh.batchDones[bid] + // earlyStopping == 1 && bh.numBeamsCBA[slot] == nBM || earlyStopping != 1 && bh.batchDones[slot] return; } } @@ -148,18 +146,18 @@ __launch_bounds__(THREADBLOCK_SIZE) __global__ { int const topKey = smemTopKV[i].key; T const topValue = smemTopKV[i].value; - bool const isEndToken = pTempId[topKey] % nV == bh.endIds[bid]; + bool const isEndToken = pTempId[topKey] % nV == bh.endIds[slot]; if (i < nBM && bh.numBeamsCBA != nullptr && isEndToken) { // Condition of this branch // This token is end-token and belongs to top nBM range in Beam search mode - int const nSeqLen = bh.sequenceLengths[bid * nBM + i] + 1 - bh.inputLengths[gbid * nBM + i]; + int const nSeqLen = bh.sequenceLengths[slot * nBM + i] + 1 - bh.inputLengths[slot * nBM + i]; float const score = applyLengthPenalty(topValue, nSeqLen, lengthPenalty); - int nCBA = bh.numBeamsCBA[gbid]; + int nCBA = bh.numBeamsCBA[slot]; if (nCBA == nBM) { // There are already nBM beams - if (score < bh.minNormedScoresCBA[gbid]) + if (score < bh.minNormedScoresCBA[slot]) { // Current score is worse than the worst one in candidate beams if (earlyStopping) @@ -179,16 +177,16 @@ __launch_bounds__(THREADBLOCK_SIZE) __global__ // Find the candidate beam index with the worst score and erase it for (int j = 0; j < nBM; j++) { - if (bh.normedScoresCBA[gbid * (nBM * 2) + j] == bh.minNormedScoresCBA[gbid]) + if (bh.normedScoresCBA[slot * (nBM * 2) + j] == bh.minNormedScoresCBA[slot]) { nCBA = j; - bh.numBeamsCBA[gbid]--; - bh.minNormedScoresCBA[gbid] = FLT_MAX; - bh.normedScoresCBA[gbid * (nBM * 2) + j] = score; + bh.numBeamsCBA[slot]--; + bh.minNormedScoresCBA[slot] = FLT_MAX; + bh.normedScoresCBA[slot * (nBM * 2) + j] = score; for (int l = 0; l < nBM; l++) { - bh.minNormedScoresCBA[gbid] - = min(bh.minNormedScoresCBA[gbid], bh.normedScoresCBA[gbid * (nBM * 2) + l]); + bh.minNormedScoresCBA[slot] + = min(bh.minNormedScoresCBA[slot], bh.normedScoresCBA[slot * (nBM * 2) + l]); } break; } @@ -198,9 +196,9 @@ __launch_bounds__(THREADBLOCK_SIZE) __global__ // Copy finished beam from work tree to CBA // The last token int indexPrev = (pTempId[topKey] / nV) % nBM; - int const step = bh.sequenceLengths[bid * nBM + indexPrev]; - int const offsetCBA = (gbid * nBM * 2 + nCBA) * bh.nMaxSeqLen; - bh.outputIdsCBA[offsetCBA + step] = bh.endIds[bid]; + int const step = bh.sequenceLengths[slot * nBM + indexPrev]; + int const offsetCBA = (slot * nBM * 2 + nCBA) * bh.nMaxSeqLen; + bh.outputIdsCBA[offsetCBA + step] = bh.endIds[slot]; if (bh.logProbsCBA != nullptr) { bh.logProbsCBA[offsetCBA + step] @@ -209,25 +207,25 @@ __launch_bounds__(THREADBLOCK_SIZE) __global__ // Previous tokens for (int j = step - 1; j >= 0; j--) { - bh.outputIdsCBA[offsetCBA + j] = bh.outputIdsPtr[bid][indexPrev * bh.nMaxSeqLen + j]; - indexPrev = bh.parentIdsPtr[bid][indexPrev * bh.nMaxSeqLen + j]; + bh.outputIdsCBA[offsetCBA + j] = bh.outputIdsPtr[slot][indexPrev * bh.nMaxSeqLen + j]; + indexPrev = bh.parentIdsPtr[slot][indexPrev * bh.nMaxSeqLen + j]; } if (bh.logProbsCBA != nullptr && bh.logProbsTiled != nullptr) { indexPrev = (pTempId[topKey] / nV) % nBM; for (int j = step - 1; j >= 0; j--) { - int const index = (j * nMBS + gbid) * nBM + indexPrev; + int const index = (j * nMBS + slot) * nBM + indexPrev; bh.logProbsCBA[offsetCBA + j] = bh.logProbsTiled[index]; - indexPrev = bh.parentIdsPtr[bid][indexPrev * bh.nMaxSeqLen + j]; + indexPrev = bh.parentIdsPtr[slot][indexPrev * bh.nMaxSeqLen + j]; } } // Other parameters - int const index = gbid * (nBM * 2) + nCBA; + int const index = slot * (nBM * 2) + nCBA; bh.sequenceLengthsCBA[index] = step; bh.normedScoresCBA[index] = score; - bh.minNormedScoresCBA[gbid] = min(bh.minNormedScoresCBA[gbid], bh.normedScoresCBA[index]); - bh.numBeamsCBA[gbid]++; + bh.minNormedScoresCBA[slot] = min(bh.minNormedScoresCBA[slot], bh.normedScoresCBA[index]); + bh.numBeamsCBA[slot]++; bh.cumLogProbsCBA[index] = (float) pTempVal[topKey]; } else if (i < nBM || bh.numBeamsCBA != nullptr && !isEndToken) @@ -236,16 +234,16 @@ __launch_bounds__(THREADBLOCK_SIZE) __global__ // 1. bh.numBeamsCBA == nullptr && i < nBM, i.e., beam search is disable // 2. bh.numBeamsCBA != nullptr && i < nBM && isEndToken == false, i.e., add token at the end // 3. bh.numBeamsCBA != nullptr && i >= nBM && isEndToken == false, i.e., add token at the end - int const step = bh.sequenceLengths[bid * nBM + nBeamForNextStep]; + int const step = bh.sequenceLengths[slot * nBM + nBeamForNextStep]; // Copy the selected token to work tree - bh.outputIdsPtr[bid][nBeamForNextStep * bh.nMaxSeqLen + step] = pTempId[topKey]; + bh.outputIdsPtr[slot][nBeamForNextStep * bh.nMaxSeqLen + step] = pTempId[topKey]; if (bh.logProbsTiled != nullptr) { - int const index = step * nMBS * nBM + bid * nBM + nBeamForNextStep; + int const index = step * nMBS * nBM + slot * nBM + nBeamForNextStep; int const indexBeam = pTempId[topKey] / nV % nBM; bh.logProbsTiled[index] = (float) pTempVal[topKey] - smemCumLogProbs[indexBeam]; } - bh.cumLogProbs[bid * nBM + nBeamForNextStep] = (float) pTempVal[topKey]; + bh.cumLogProbs[slot * nBM + nBeamForNextStep] = (float) pTempVal[topKey]; nBeamForNextStep++; } else @@ -262,7 +260,7 @@ __launch_bounds__(THREADBLOCK_SIZE) __global__ // 2. In EarlyStopping mode, and get enough tokens for the next generation step // 3. In NonEarlyStopping mode, and get enough tokens for the next generation step // TODO: improve the condition like below - // earlyStopping == 1 && bh.numBeamsCBA[gbid] >= nBM || nBeamForNextStep >= nBM + // earlyStopping == 1 && bh.numBeamsCBA[slot] >= nBM || nBeamForNextStep >= nBM break; } } @@ -271,31 +269,31 @@ __launch_bounds__(THREADBLOCK_SIZE) __global__ // Update bh.batchDones if (tid == 0 && bh.numBeamsCBA != nullptr) { - if (bh.numBeamsCBA[bid] < nBM) + if (bh.numBeamsCBA[slot] < nBM) { // no enough beams - bh.batchDones[bid] = false; + bh.batchDones[slot] = false; } else if (earlyStopping == 1) { // enough candidate beams in EarlyStopping mode - bh.batchDones[bid] = true; + bh.batchDones[slot] = true; } else { // enough beams in NonEarlyStopping mode - int nSeqLen = bh.sequenceLengths[bid * nBM] + 1 - bh.inputLengths[gbid * nBM]; + int nSeqLen = bh.sequenceLengths[slot * nBM] + 1 - bh.inputLengths[slot * nBM]; float const bestCumLogProbs = smemTopKV[0].value; // According to semantics of HF, smemTopKV[0].value is used as bestCumLogProbs - // But maybe bh.cumLogProbs[bid * nBM + i] is more suitable? + // But maybe bh.cumLogProbs[slot * nBM + i] is more suitable? // https://github.com/huggingface/transformers/blob/main/src/transformers/generation/beam_search.py#L307 if (earlyStopping != 0 && lengthPenalty > 0.0f) { // Specialization for earlyStopping == "never" and lengthPenalty > 0 in HF - nSeqLen = bh.nMaxSeqLen - bh.inputLengths[gbid * nBM]; + nSeqLen = bh.nMaxSeqLen - bh.inputLengths[slot * nBM]; } float const bestAttainableScore = applyLengthPenalty(bestCumLogProbs, nSeqLen, lengthPenalty); - bh.batchDones[bid] = bh.minNormedScoresCBA[gbid] >= bestAttainableScore; + bh.batchDones[slot] = bh.minNormedScoresCBA[slot] >= bestAttainableScore; } } __syncthreads(); @@ -304,32 +302,33 @@ __launch_bounds__(THREADBLOCK_SIZE) __global__ __shared__ int smemSeqLen[PAD_2K / 2]; if (tid < nBM) { - smemSeqLen[tid] = bh.sequenceLengths[bid * nBM + tid]; + smemSeqLen[tid] = bh.sequenceLengths[slot * nBM + tid]; } __syncthreads(); if (tid < nBM) { - int const indexBatchBeam = bid * nBM + tid; + int const indexBatchBeam = slot * nBM + tid; int const step = smemSeqLen[tid]; if (!bh.finished[indexBatchBeam].isFinished()) { smemSeqLen[tid]++; } - int const newId = bh.outputIdsPtr[bid][tid * bh.nMaxSeqLen + step]; + int const newId = bh.outputIdsPtr[slot][tid * bh.nMaxSeqLen + step]; int const newBeamId = (newId / nV) % nBM; int const newTokenId = newId % nV; bh.sequenceLengths[indexBatchBeam] = smemSeqLen[newBeamId]; - if (newTokenId == bh.endIds[bid]) + if (newTokenId == bh.endIds[slot]) { bh.finished[indexBatchBeam].setFinishedEOS(); } - bh.parentIdsPtr[bid][tid * bh.nMaxSeqLen + step] = newBeamId; - bh.outputIdsPtr[bid][tid * bh.nMaxSeqLen + step] = newTokenId; - if ((earlyStopping == 1) && (bh.numBeamsCBA != nullptr && bh.numBeamsCBA[gbid] == nBM) - || (earlyStopping != 1) && bh.batchDones[bid]) + bh.parentIdsPtr[slot][tid * bh.nMaxSeqLen + step] = newBeamId; + bh.outputIdsPtr[slot][tid * bh.nMaxSeqLen + step] = newTokenId; + + if ((earlyStopping == 1) && (bh.numBeamsCBA != nullptr && bh.numBeamsCBA[slot] == nBM) + || (earlyStopping != 1) && bh.batchDones[slot]) { - bh.batchDones[bid] = true; + bh.batchDones[slot] = true; bh.finished[indexBatchBeam].setFinished(); } } @@ -350,174 +349,19 @@ __device__ __forceinline__ MD reduce_md_op(MD a, MD b) return res; } -template -struct TopKMD -{ - MD md; - TopK topk; -}; - -template -__device__ __forceinline__ TopKMD reduce_topk_md_op(TopKMD const& a, TopKMD const& b) -{ - TopKMD res; - res.md = reduce_md_op(a.md, b.md); - res.topk = reduce_topk_op(a.topk, b.topk); - return res; -} - -template -__launch_bounds__(THREADBLOCK_SIZE) __global__ void beamKernel(T const* __restrict logits, T const* __restrict bias, - int* __restrict pTempId, T* __restrict pTempVal, BeamHypotheses bh) -{ - int const tid = threadIdx.x; - int const bid = blockIdx.x; - int const nBM{bh.nBeamWidth}; - int const nV{bh.nVocabSize}; - int const* endIds{bh.endIds}; - float const* cumLogProbs{bh.cumLogProbs}; - FinishedState const* finished{bh.finished}; - T const MAX_T_VAL = std::is_same_v ? HALF_FLT_MAX : FLT_MAX; - - TopKMD partial; - partial.md.m = -MAX_T_VAL; - partial.md.d = 0.0F; - partial.topk.init(); - - if (finished[bid].isFinished()) - { - for (int i = tid; i < nV; i += THREADBLOCK_SIZE) - { - float const val = i == endIds[bid / nBM] ? MAX_T_VAL : -MAX_T_VAL; - partial.md = reduce_md_op(partial.md, {val, 1.0F}); - partial.topk.insert(val, i); - } - } - else - { - T const* local_logits = logits + bid * nV; - for (int i = tid; i < nV; i += THREADBLOCK_SIZE) - { - float const val = local_logits[i] + bias[i]; - partial.md = reduce_md_op(partial.md, {val, 1.0F}); - partial.topk.insert(val, i); - } - } - - typedef cub::BlockReduce, THREADBLOCK_SIZE> BlockReduce; - __shared__ typename BlockReduce::TempStorage smemReduceBuffer; - - TopKMD total = BlockReduce(smemReduceBuffer).Reduce(partial, reduce_topk_md_op); - - if (tid == 0) - { - int* localTopKId = pTempId + bid * nBM; - T const* localTopKVal = pTempVal + bid * nBM; - float const total_m = total.md.m; - float const total_d = logf(total.md.d); - float localCumLogProbs = cumLogProbs[bid]; - for (int i = 0; i < nBM; ++i) - { - localTopKId[i] = total.topk.p[i] + bid * nV; - localTopKVal[i] = total.topk.u[i] - total_m - total_d + localCumLogProbs; - } - } -} - template __launch_bounds__(THREADBLOCK_SIZE, 1) __global__ - void beamStage1BaseKernel(T const* __restrict logits, T const* __restrict bias, float* __restrict pTemp, - int const* __restrict endIds, FinishedState const* __restrict finished, int nBM, int nV) + void beamStage1Kernel(T const* __restrict logits, T const* __restrict bias, float* __restrict pTemp, + int const* __restrict endIds, FinishedState const* __restrict finished, int const nV, int const nVLocal, + runtime::SizeType32 const* batchSlots, int dyn_smem_size) { - // Compare to beamStage1FastKernel, here is no share memory for storage of logits, - // and each ThreadBlock is responsible for `nV / nVPart` elements - constexpr int PACKED_TOP_KMD_SIZE = 2 * PAD_2K + 2; + constexpr auto PACKED_TOP_KMD_SIZE = 2 * PAD_2K + 2; + int const nBM = gridDim.y; int const tid = threadIdx.x; - int const bid = blockIdx.x; - int const nVLocal = (nV + gridDim.y - 1) / gridDim.y; - int const section_start = nVLocal * blockIdx.y; - int const section_end = std::min(section_start + nVLocal, nV); - T const MAX_T_VAL = std::is_same_v ? HALF_FLT_MAX : FLT_MAX; - - // Load element from logits to do reduce_md and argmax meanwhile -#if TOPK_FP16_STORAGE == 1 - TopKMD<__half, PAD_2K> partial; -#else - TopKMD partial; -#endif - partial.md.m = -MAX_T_VAL; - partial.md.d = 0.0F; - partial.topk.init(); - - if (finished[bid].isFinished()) - { -#pragma unroll 1 - for (int i = section_start + tid; i < section_end; i += THREADBLOCK_SIZE) - { - float const val = (i == endIds[bid / nBM]) ? MAX_T_VAL : -MAX_T_VAL; - MD const new_elem_md{val, 1.0F}; - partial.md = reduce_md_op(partial.md, new_elem_md); - partial.topk.insert(val, i); - } - } - else - { - T const* local_logits = logits + bid * nV; -#pragma unroll 1 - for (int i = section_start + tid; i < section_end; i += THREADBLOCK_SIZE) - { - T const b = bias == nullptr ? (T) 0.0f : bias[i]; - T const val = local_logits[i] + b; - MD new_elem_md{val, 1.0F}; - partial.md = reduce_md_op(partial.md, new_elem_md); - partial.topk.insert(val, i); - } - } - - // Search the top 2K elements among `nV` elements and write into smemOutput -#if TOPK_FP16_STORAGE == 1 - typedef cub::BlockReduce, THREADBLOCK_SIZE> BlockReduce; - __shared__ typename BlockReduce::TempStorage smemReduceBuffer; - TopKMD<__half, PAD_2K> total = BlockReduce(smemReduceBuffer).Reduce(partial, reduce_topk_md_op<__half, PAD_2K>); -#else - typedef cub::BlockReduce, THREADBLOCK_SIZE> BlockReduce; - __shared__ typename BlockReduce::TempStorage smemReduceBuffer; - TopKMD total = BlockReduce(smemReduceBuffer).Reduce(partial, reduce_topk_md_op); -#endif - __shared__ float smemOutput[PACKED_TOP_KMD_SIZE]; - - if (tid == 0) - { - for (int i = 0; i < 2 * nBM; i++) - { - int const index = bid * nV + total.topk.p[i]; - reinterpret_cast(smemOutput)[i] = index; - smemOutput[PAD_2K + i] = total.topk.u[i]; - } - smemOutput[2 * PAD_2K] = total.md.d; - smemOutput[2 * PAD_2K + 1] = total.md.m; - } - __syncthreads(); - - // Write the smemOutput into pTemp - float* local_temp_buffer = pTemp + bid * PACKED_TOP_KMD_SIZE * gridDim.y + blockIdx.y * PACKED_TOP_KMD_SIZE; -#pragma unroll - for (int i = tid; i < PACKED_TOP_KMD_SIZE; i += THREADBLOCK_SIZE) - { - local_temp_buffer[i] = smemOutput[i]; - } -} - -template -__launch_bounds__(THREADBLOCK_SIZE, 1) __global__ void beamStage1FastKernel(T const* __restrict logits, - T const* __restrict bias, float* __restrict pTemp, int const* __restrict endIds, - FinishedState const* __restrict finished, int const nBM, int const nV, int const nVLocal) -{ - constexpr int PACKED_TOP_KMD_SIZE = 2 * PAD_2K + 2; - int const tid = threadIdx.x; - int const bid = blockIdx.x; - int const section_start = nVLocal * blockIdx.y; + int const slot = batchSlots ? batchSlots[blockIdx.x] : blockIdx.x; + int const section_start = nVLocal * blockIdx.z; int const section_end = std::min(section_start + nVLocal, nV); + auto const nVOffset = (blockIdx.x * nBM + blockIdx.y) * nV; int const valid_smem_length = section_end - section_start; T const MAX_T_VAL = std::is_same_v ? HALF_FLT_MAX : FLT_MAX; @@ -533,15 +377,15 @@ __launch_bounds__(THREADBLOCK_SIZE, 1) __global__ void beamStage1FastKernel(T co #else using KVPair = cub::KeyValuePair; #endif - KVPair topKVPairPartial{nV - 1, -MAX_T_VAL}; + + KVPair topKVPairPartial{-1, -MAX_T_VAL}; cub::ArgMax argmax; - if (finished[bid].isFinished()) + if (finished[slot * nBM + blockIdx.y].isFinished()) { -#pragma unroll 1 for (int i = section_start + tid; i < section_end; i += THREADBLOCK_SIZE) { - float const val = (i == endIds[bid / nBM]) ? MAX_T_VAL : -MAX_T_VAL; + float const val = (i == endIds[slot]) ? MAX_T_VAL : -MAX_T_VAL; int const smem_index = i - section_start; smemLogProbs[smem_index] = val; MD const new_elem_md{val, 1.0F}; @@ -552,12 +396,10 @@ __launch_bounds__(THREADBLOCK_SIZE, 1) __global__ void beamStage1FastKernel(T co } else { - T const* local_logits = logits + bid * nV; -#pragma unroll 1 for (int i = section_start + tid; i < section_end; i += THREADBLOCK_SIZE) { T const b = bias == nullptr ? (T) 0.0f : bias[i]; - T const val = local_logits[i] + b; + T const val = logits[nVOffset + i] + b; int const smem_index = i - section_start; smemLogProbs[smem_index] = val; MD new_elem_md{val, 1.0F}; @@ -566,6 +408,7 @@ __launch_bounds__(THREADBLOCK_SIZE, 1) __global__ void beamStage1FastKernel(T co topKVPairPartial = argmax(topKVPairPartial, new_elem_topk); } } + __syncthreads(); // Search the top 2K elements among `nVLocal` elements of this ThreadBlock and write into smemOutput @@ -587,7 +430,7 @@ __launch_bounds__(THREADBLOCK_SIZE, 1) __global__ void beamStage1FastKernel(T co KVPair topKVPair = BlockReduceTopK(smemReduceBuffer.topk).Reduce(topKVPairPartial, argmax); if (tid == 0) { - int const index = bid * nV + section_start + topKVPair.key; + int const index = nVOffset + section_start + topKVPair.key; reinterpret_cast(smemOutput)[i] = index; smemOutput[PAD_2K + i] = topKVPair.value; smemLogProbs[topKVPair.key] = -MAX_T_VAL; // pollute the value of the popped element @@ -606,11 +449,13 @@ __launch_bounds__(THREADBLOCK_SIZE, 1) __global__ void beamStage1FastKernel(T co topKVPairPartial = argmax(topKVPairPartial, {index, smemLogProbs[index]}); } } + + // Sync due to threadToUpdate RAW dependency + __syncthreads(); } // Do reduce_md among the top 2K elements in the smemOutput and write into tail of smemOutput - auto reduce_md_func = [](const MD& a, const MD& b) { return reduce_md_op(a, b); }; - MD total_md = BlockReduceMD(smemReduceBuffer.md).Reduce(partial_md, reduce_md_func); + MD total_md = BlockReduceMD(smemReduceBuffer.md).Reduce(partial_md, reduce_md_op); if (tid == 0) { smemOutput[2 * PAD_2K] = total_md.d; @@ -619,8 +464,8 @@ __launch_bounds__(THREADBLOCK_SIZE, 1) __global__ void beamStage1FastKernel(T co __syncthreads(); // Write the smemOutput into pTemp - float* local_temp_buffer = pTemp + bid * PACKED_TOP_KMD_SIZE * gridDim.y + blockIdx.y * PACKED_TOP_KMD_SIZE; -#pragma unroll + float* local_temp_buffer + = pTemp + (blockIdx.x * nBM + blockIdx.y) * PACKED_TOP_KMD_SIZE * gridDim.z + blockIdx.z * PACKED_TOP_KMD_SIZE; for (int i = tid; i < PACKED_TOP_KMD_SIZE; i += THREADBLOCK_SIZE) { local_temp_buffer[i] = smemOutput[i]; @@ -628,12 +473,15 @@ __launch_bounds__(THREADBLOCK_SIZE, 1) __global__ void beamStage1FastKernel(T co } template -__launch_bounds__(THREADBLOCK_SIZE) __global__ void beamStage2Kernel(int* __restrict pTempId, T* __restrict pTempVal, - float* __restrict pTemp, float const* __restrict cumLogProbs, int const nBM, int const nV, int const nVPart) +__launch_bounds__(THREADBLOCK_SIZE) __global__ + void beamStage2Kernel(int* __restrict pTempId, T* __restrict pTempVal, float* __restrict pTemp, + float const* __restrict cumLogProbs, int const nV, int const nVPart, runtime::SizeType32 const* batchSlots) { constexpr int PACKED_TOP_KMD_SIZE = 2 * PAD_2K + 2; - int const bid = blockIdx.x; + auto const nBM = gridDim.y; + auto const gbid = blockIdx.x * gridDim.y + blockIdx.y; int const tid = threadIdx.x; + auto const slot = batchSlots ? batchSlots[blockIdx.x] : blockIdx.x; T const MAX_T_VAL = std::is_same_v ? HALF_FLT_MAX : FLT_MAX; using KVPair = cub::KeyValuePair; @@ -653,10 +501,8 @@ __launch_bounds__(THREADBLOCK_SIZE) __global__ void beamStage2Kernel(int* __rest MD partial_md{-MAX_T_VAL, 0.0f}; KVPair topKVPair{nV - 1, -MAX_T_VAL}; - auto reduce_md_func = [](const MD& a, const MD& b) { return reduce_md_op(a, b); }; - // Load and unpack into registers through smem - float* localTempBuffer = pTemp + PACKED_TOP_KMD_SIZE * bid * nVPart; + float* localTempBuffer = pTemp + PACKED_TOP_KMD_SIZE * gbid * nVPart; if constexpr (IS_FAST_KERNEL) // Use share memory instead of global memory { extern __shared__ char smem[]; @@ -710,26 +556,23 @@ __launch_bounds__(THREADBLOCK_SIZE) __global__ void beamStage2Kernel(int* __rest } __syncthreads(); - MD total_md = BlockReduceMD(smemReduceBuffer.md).Reduce(partial_md, reduce_md_func); + MD total_md = BlockReduceMD(smemReduceBuffer.md).Reduce(partial_md, reduce_md_op); if (tid == 0) { float d_total_log = logf(total_md.d); + auto const cumLogProbsValue = cumLogProbs[slot * nBM + blockIdx.y]; - for (int i = 0; i < PAD_2K; ++i) + for (int i = 0; i < 2 * nBM; ++i) { float val = (float) buf_smem_kv[i].value - total_md.m - d_total_log; - if (i < 2 * nBM) - { - pTempId[bid * 2 * nBM + i] = buf_smem_kv[i].key; - pTempVal[bid * 2 * nBM + i] = val + cumLogProbs[bid]; - } + pTempId[gbid * 2 * nBM + i] = buf_smem_kv[i].key; + pTempVal[gbid * 2 * nBM + i] = val + cumLogProbsValue; } } } #define BEAM_STAGE2_KERNEL(N_VOCAB_PART, IS_FAST_KERNEL) \ - do \ { \ if (IS_FAST_KERNEL && nShareMemory >= (48 << 10)) \ { \ @@ -737,14 +580,15 @@ __launch_bounds__(THREADBLOCK_SIZE) __global__ void beamStage2Kernel(int* __rest cudaFuncAttributeMaxDynamicSharedMemorySize, nShareMemory)); \ } \ beamStage2Kernel \ - <<>>( \ - pTempId, pTempVal, pTemp, cumLogProbs, nBM, nV, nVPart); \ - } while (0); \ + <<>>( \ + pTempId, pTempVal, pTemp, cumLogProbs, nV, nVPart, batchSlots); \ + } \ return; template __inline__ void beamStage2KernelLauncher(float* pTemp, float const* cumLogProbs, int* pTempId, T* pTempVal, - int const nBS, int const nBM, int const nVPart, int const nV, int const max_smem_per_block, cudaStream_t stream) + int const nBS, int const nBM, int const nVPart, int const nV, int const max_smem_per_block, cudaStream_t stream, + runtime::SizeType32 const* batchSlots) { // TODO: rewrite kernel to remove dependence of constant block size to reduce compilation time size_t const nShareMemory = sizeof(float) * nVPart * (2 * PAD_2K + 2) + sizeof(cub::KeyValuePair) * PAD_2K; @@ -813,6 +657,7 @@ void topKSoftMaxKernelLauncher(T const* logits, T const* bias, void* workspace, int const nBM{bh.nBeamWidth}; int const nV{bh.nVocabSize}; int const* endIds{bh.endIds}; + runtime::SizeType32 const* batchSlots{bh.batchSlots}; FinishedState const* finished{bh.finished}; int const offset = roundUp(nBS * nBM * nBM * 2, 4); @@ -820,12 +665,10 @@ void topKSoftMaxKernelLauncher(T const* logits, T const* bias, void* workspace, T* pTempVal = reinterpret_cast(pTempId + offset); float* pTemp = reinterpret_cast(pTempVal + offset); -#ifdef DO_SPLIT_SMALL_TOP_K_SOFTMAX - // Upper limit count of ThreadBlock, gotten by using no share memory int max_active_blocks = -1; TLLM_CUDA_CHECK(cudaOccupancyMaxActiveBlocksPerMultiprocessor( - &max_active_blocks, beamStage1FastKernel, nBlockSize, 0)); + &max_active_blocks, beamStage1Kernel, nBlockSize, 0)); // Find the max smem on the device and use that to determine the vocab parts in the best case. int max_smem_per_sm = -1; @@ -834,7 +677,7 @@ void topKSoftMaxKernelLauncher(T const* logits, T const* bias, void* workspace, TLLM_CUDA_CHECK(cudaDeviceGetAttribute(&max_smem_per_sm, cudaDevAttrMaxSharedMemoryPerMultiprocessor, device)); TLLM_CUDA_CHECK(cudaDeviceGetAttribute(&max_smem_per_block, cudaDevAttrMaxSharedMemoryPerBlockOptin, device)); cudaFuncAttributes attr; - TLLM_CUDA_CHECK(cudaFuncGetAttributes(&attr, beamStage1FastKernel)); + TLLM_CUDA_CHECK(cudaFuncGetAttributes(&attr, beamStage1Kernel)); // One ThreadBlock must at least have share memory of `sizeof(T) * nV / nMaxVocabPartForStage1FastKernel` bytes int const static_smem = attr.sharedSizeBytes; @@ -855,47 +698,22 @@ void topKSoftMaxKernelLauncher(T const* logits, T const* bias, void* workspace, nVPart = ceilDiv(sizeof(T) * nV, dyn_smem_size); } - if (nVPart <= nMaxVocabPartForStage1FastKernel) - { - // Use stage 1 fast kernel - int const nVocabChunk = (nV + nVPart - 1) / nVPart; - int const dyn_smem_size = sizeof(T) * nVocabChunk; - if (dyn_smem_size >= (48 << 10)) - { - TLLM_CUDA_CHECK(cudaFuncSetAttribute(beamStage1FastKernel, - cudaFuncAttributeMaxDynamicSharedMemorySize, dyn_smem_size)); - } - dim3 gridSize(nBS * nBM, nVPart); - beamStage1FastKernel - <<>>( - logits, bias, pTemp, endIds, finished, nBM, nV, nVocabChunk); - } - else + int const nVocabChunk = (nV + nVPart - 1) / nVPart; + int const dyn_smem_size = sizeof(T) * nVocabChunk; + if (dyn_smem_size >= (48 << 10)) { - // Use stage 1 base kernel, useless branch now - int nVPart = 4; - if (nBS * nBM < 256) - { - // TODO: add heuristics for base stage 1 kernel - // Volta has 80 SMs, so we aim for three waves - nVPart = (240 + nBS * nBM - 1) / (nBS * nBM); - nVPart = std::min(128, nVPart); // we implement up to 128 - } - cudaFuncSetAttribute(beamStage1BaseKernel, - cudaFuncAttributePreferredSharedMemoryCarveout, cudaSharedmemCarveoutMaxL1); - dim3 gridSize(nBS * nBM, nVPart); - beamStage1BaseKernel - <<>>(logits, bias, pTemp, endIds, finished, nBM, nV); + TLLM_CUDA_CHECK(cudaFuncSetAttribute(beamStage1Kernel, + cudaFuncAttributeMaxDynamicSharedMemorySize, dyn_smem_size)); } + + dim3 gridSize(nBS, nBM, nVPart); + beamStage1Kernel<<>>( + logits, bias, pTemp, endIds, finished, nV, nVocabChunk, batchSlots, dyn_smem_size); + sync_check_cuda_error(); beamStage2KernelLauncher( - pTemp, bh.cumLogProbs, pTempId, pTempVal, nBS, nBM, nVPart, nV, max_smem_per_block, stream); - -#else - beamKernel - <<>>(logits, bias, pTempId, pTempVal, bh); -#endif + pTemp, bh.cumLogProbs, pTempId, pTempVal, nBS, nBM, nVPart, nV, max_smem_per_block, stream, batchSlots); sync_check_cuda_error(); diff --git a/cpp/tensorrt_llm/kernels/customAllReduceKernels.cu b/cpp/tensorrt_llm/kernels/customAllReduceKernels.cu index cfa8fd9ce..baacc9e5c 100644 --- a/cpp/tensorrt_llm/kernels/customAllReduceKernels.cu +++ b/cpp/tensorrt_llm/kernels/customAllReduceKernels.cu @@ -122,24 +122,16 @@ __inline__ __device__ void multi_gpu_barrier(uint32_t** signals, uint32_t const // Dimension 0 is the "listening" dimension, dimension 1 is "emitting" dimension // Block 0 broadcasts its flag (local_rank on emitting dimension) to all receivers + size_t offset = (flag % 2) ? world_size : 0; + if (bidx == 0) { - st_flag_release(flag, signals[tidx] + local_rank); + st_flag_release(flag, signals[tidx] + offset + local_rank); } // All blocks check that corresponding block 0 on other GPUs have set the flag // No deadlock because block #0 is always the first block started - uint32_t* peer_barrier_d = signals[local_rank] + tidx; - while (ld_flag_acquire(peer_barrier_d) != flag) - { - } - - if (bidx == 0) - { - st_flag_release(flag, signals[tidx] + world_size + local_rank); - } - - peer_barrier_d = signals[local_rank] + world_size + tidx; + uint32_t* peer_barrier_d = signals[local_rank] + offset + tidx; while (ld_flag_acquire(peer_barrier_d) != flag) { } @@ -160,20 +152,16 @@ __inline__ __device__ void block_barrier(uint32_t** signals, uint32_t const flag // Block broadcast its flag (local_rank on emitting dimension) to all receivers uint32_t flag_block_offset = world_size + bidx * world_size; - st_flag_release(flag, signals[tidx] + flag_block_offset + local_rank); - // Blocks check that corresponding blocks on other GPUs have also set the flag - uint32_t* peer_barrier_d = signals[local_rank] + flag_block_offset + tidx; - - while (ld_flag_acquire(peer_barrier_d) != flag) + if (flag % 2 == 1) { + flag_block_offset += (grid_size + 1) * world_size; } - flag_block_offset += (grid_size + 1) * world_size; st_flag_release(flag, signals[tidx] + flag_block_offset + local_rank); // Blocks check that corresponding blocks on other GPUs have also set the flag - peer_barrier_d = signals[local_rank] + flag_block_offset + tidx; + uint32_t* peer_barrier_d = signals[local_rank] + flag_block_offset + tidx; while (ld_flag_acquire(peer_barrier_d) != flag) { diff --git a/cpp/tensorrt_llm/kernels/decoderMaskedMultiheadAttention.h b/cpp/tensorrt_llm/kernels/decoderMaskedMultiheadAttention.h index 6ce276827..38cc540c4 100644 --- a/cpp/tensorrt_llm/kernels/decoderMaskedMultiheadAttention.h +++ b/cpp/tensorrt_llm/kernels/decoderMaskedMultiheadAttention.h @@ -78,7 +78,7 @@ struct Multihead_attention_params_base { // The output buffer. Dimensions B x D. - T* out = nullptr; + void* out = nullptr; // The input Qs and the associated bias. Dimensions B x D and D, resp. T const *q = nullptr, *q_bias = nullptr; diff --git a/cpp/tensorrt_llm/kernels/decoderMaskedMultiheadAttention/decoderMaskedMultiheadAttentionTemplate.h b/cpp/tensorrt_llm/kernels/decoderMaskedMultiheadAttention/decoderMaskedMultiheadAttentionTemplate.h index a4e45a004..cc2898ed3 100644 --- a/cpp/tensorrt_llm/kernels/decoderMaskedMultiheadAttention/decoderMaskedMultiheadAttentionTemplate.h +++ b/cpp/tensorrt_llm/kernels/decoderMaskedMultiheadAttention/decoderMaskedMultiheadAttentionTemplate.h @@ -2662,7 +2662,7 @@ __global__ void __launch_bounds__(MAX_THEADS_PER_BLOCK, MIN_BLOCKS_PER_SM) maske // This makes sure we have coalesced memory access. V_vec_k final_out; convert_from_float(&final_out, out); - *reinterpret_cast(¶ms.out[bhvi]) = final_out; + *reinterpret_cast(static_cast(params.out) + bhvi) = final_out; } } else @@ -2680,7 +2680,7 @@ __global__ void __launch_bounds__(MAX_THEADS_PER_BLOCK, MIN_BLOCKS_PER_SM) maske convert_from_float(reinterpret_cast(¶ms.partial_sum[partial_stats_offset]), sum); } #else // MMHA_USE_FP32_ACCUM_FOR_OUT - *reinterpret_cast(¶ms.out[bhvi]) = out; + *reinterpret_cast(static_cast(params.out) + bhvi) = out; #endif // MMHA_USE_FP32_ACCUM_FOR_OUT } @@ -2838,7 +2838,7 @@ __global__ void __launch_bounds__(MAX_THEADS_PER_BLOCK, MIN_BLOCKS_PER_SM) maske } else { - *reinterpret_cast(¶ms.out[bhi * Dh + oi]) = thread_accumulated_out; + *reinterpret_cast(static_cast(params.out) + (bhi * Dh + oi)) = thread_accumulated_out; } } diff --git a/cpp/tensorrt_llm/kernels/decoderMaskedMultiheadAttention/decoderXQAImplJIT/compileEngine.cpp b/cpp/tensorrt_llm/kernels/decoderMaskedMultiheadAttention/decoderXQAImplJIT/compileEngine.cpp index c38db683d..4797701e4 100644 --- a/cpp/tensorrt_llm/kernels/decoderMaskedMultiheadAttention/decoderXQAImplJIT/compileEngine.cpp +++ b/cpp/tensorrt_llm/kernels/decoderMaskedMultiheadAttention/decoderXQAImplJIT/compileEngine.cpp @@ -20,6 +20,7 @@ #include "tensorrt_llm/common/assert.h" #include "tensorrt_llm/common/stringUtils.h" #include "tensorrt_llm/common/tllmException.h" +#include "tensorrt_llm/kernels/decoderMaskedMultiheadAttention/decoderXQAImplJIT/kernelUtils.h" #include #include @@ -51,6 +52,7 @@ namespace jit CubinObj CompileEngine::compile() const { tllmXqaJitProgram program; + bool useQGMMAKernel = supportConfigQGMMA(mXqaParams, mSM, true); tllmXqaJitContext context{/*sm=*/mSM, /*head_size=*/static_cast(mXqaParams.head_size), /*num_q_heads=*/static_cast(mXqaParams.num_q_heads), @@ -60,7 +62,8 @@ CubinObj CompileEngine::compile() const /*multi_query_tokens=*/mXqaParams.multi_query_tokens, /*paged_kv_cache=*/mXqaParams.paged_kv_cache, /*data_type=*/static_cast(mXqaParams.data_type), - /*kv_cache_data_type=*/static_cast(mXqaParams.kv_cache_data_type)}; + /*kv_cache_data_type=*/static_cast(mXqaParams.kv_cache_data_type), + /*kernel_type=*/useQGMMAKernel ? TLLM_XQA_JIT_QGMMA : TLLM_XQA_JIT_HMMA}; CHECK_TLLM_XQA_JIT_ERROR(tllmXqaJitCreateAndCompileProgram(&program, &context)); diff --git a/cpp/tensorrt_llm/kernels/decoderMaskedMultiheadAttention/decoderXQAImplJIT/cubinObj.cpp b/cpp/tensorrt_llm/kernels/decoderMaskedMultiheadAttention/decoderXQAImplJIT/cubinObj.cpp index e974b75ee..d2f072cbc 100644 --- a/cpp/tensorrt_llm/kernels/decoderMaskedMultiheadAttention/decoderXQAImplJIT/cubinObj.cpp +++ b/cpp/tensorrt_llm/kernels/decoderMaskedMultiheadAttention/decoderXQAImplJIT/cubinObj.cpp @@ -70,6 +70,50 @@ CubinObj& CubinObj::operator=(CubinObj const& other) return *this; } +CubinObj::CubinObj(CubinObj&& other) +{ + this->mContent = std::move(other.mContent); + if (other.mInitialized) + { + this->mInitialized = true; + this->mDriver = std::move(other.mDriver); + this->mModule = other.mModule; + this->mFunction = other.mFunction; + this->mSharedMemBytes = other.mSharedMemBytes; + + other.mInitialized = false; + } + else + { + this->mInitialized = false; + } +} + +CubinObj& CubinObj::operator=(CubinObj&& other) +{ + if (this == &other) + { + return *this; + } + + this->mContent = std::move(other.mContent); + if (other.mInitialized) + { + this->mInitialized = true; + this->mDriver = std::move(other.mDriver); + this->mModule = other.mModule; + this->mFunction = other.mFunction; + this->mSharedMemBytes = other.mSharedMemBytes; + + other.mInitialized = false; + } + else + { + this->mInitialized = false; + } + return *this; +} + size_t CubinObj::getSerializationSize() const noexcept { size_t result = sizeof(uint32_t) + mContent.size(); @@ -129,6 +173,15 @@ void CubinObj::initialize() } } +CubinObj::~CubinObj() +{ + if (mInitialized) + { + cuErrCheck(mDriver->cuModuleUnload(mModule), mDriver); + mInitialized = false; + } +} + } // namespace jit } // namespace kernels } // namespace tensorrt_llm diff --git a/cpp/tensorrt_llm/kernels/decoderMaskedMultiheadAttention/decoderXQAImplJIT/cubinObj.h b/cpp/tensorrt_llm/kernels/decoderMaskedMultiheadAttention/decoderXQAImplJIT/cubinObj.h index e5742112a..29867c09b 100644 --- a/cpp/tensorrt_llm/kernels/decoderMaskedMultiheadAttention/decoderXQAImplJIT/cubinObj.h +++ b/cpp/tensorrt_llm/kernels/decoderMaskedMultiheadAttention/decoderXQAImplJIT/cubinObj.h @@ -40,8 +40,9 @@ class CubinObj CubinObj& operator=(CubinObj const& other); // CubinObj can be move-constructed/assigned. - CubinObj(CubinObj&& other) = default; - CubinObj& operator=(CubinObj&& other) = default; + CubinObj(CubinObj&& other); + CubinObj& operator=(CubinObj&& other); + ~CubinObj(); // Should be called at least once before calling launch(). void initialize(); diff --git a/cpp/tensorrt_llm/kernels/decoderMaskedMultiheadAttention/decoderXQAImplJIT/decoderXQAImplJIT.cpp b/cpp/tensorrt_llm/kernels/decoderMaskedMultiheadAttention/decoderXQAImplJIT/decoderXQAImplJIT.cpp index fc7f1897d..f2a47407b 100644 --- a/cpp/tensorrt_llm/kernels/decoderMaskedMultiheadAttention/decoderXQAImplJIT/decoderXQAImplJIT.cpp +++ b/cpp/tensorrt_llm/kernels/decoderMaskedMultiheadAttention/decoderXQAImplJIT/decoderXQAImplJIT.cpp @@ -19,6 +19,8 @@ #include "tensorrt_llm/common/envUtils.h" #include "tensorrt_llm/kernels/decoderMaskedMultiheadAttention/cubin/xqa_kernel_cubin.h" #include "tensorrt_llm/kernels/decoderMaskedMultiheadAttention/decoderXQAConstants.h" +#include "tensorrt_llm/kernels/decoderMaskedMultiheadAttention/decoderXQAImplJIT/decoderXQAImplJIT.h" +#include "tensorrt_llm/kernels/decoderMaskedMultiheadAttention/decoderXQAImplJIT/kernelUtils.h" #include "tensorrt_llm/kernels/decoderMaskedMultiheadAttention/decoderXQARunner.h" #include "tensorrt_llm/kernels/decoderMaskedMultiheadAttention/tensorMapUtils.h" #include "tensorrt_llm/kernels/unfusedAttentionKernels.h" @@ -49,28 +51,12 @@ DecoderXQAImplJIT::DecoderXQAImplJIT(DecoderXQARunner* runner) , mForceXQA(tensorrt_llm::common::forceXQAKernels()) , mSM(tensorrt_llm::common::getSMVersion()) { - initSupportedConfigs(); -} - -void DecoderXQAImplJIT::initSupportedConfigs() -{ - mSupportedConfigs.clear(); - - size_t nbConfigs = sizeof(sXqaKernelMetaInfo) / sizeof(sXqaKernelMetaInfo[0]); - for (size_t i = 0; i < nbConfigs; ++i) - { - XQAKernelMetaInfo const& kernelMeta = sXqaKernelMetaInfo[i]; - if (!kernelMeta.mMultiQueryTokens) - { - // Exclude medusa kernels from JIT because they are compiled from a different CUDA source file. - mSupportedConfigs.insert(getRuntimeHashKeyFromKernelMeta(kernelMeta)); - } - } } -bool DecoderXQAImplJIT::supportConfig(XQAParams const& xqaParams) const +bool DecoderXQAImplJIT::supportConfig(XQAParams const& xqaParams, bool forConfigurePlugin) const { - return mSupportedConfigs.find(getRuntimeHashKeyFromXQAParams(xqaParams)) != mSupportedConfigs.end(); + return jit::supportConfigQGMMA(xqaParams, mSM, forConfigurePlugin) + || jit::supportConfigHMMA(xqaParams, mSM, forConfigurePlugin); } bool DecoderXQAImplJIT::mayHavePerfGain(XQAParams const& xqaParams) const @@ -92,16 +78,25 @@ bool DecoderXQAImplJIT::mayHavePerfGain(XQAParams const& xqaParams) const return static_cast(block_count) * kEnableMinBlockFactor >= static_cast(mRunner->mMultiProcessorCount); } -bool DecoderXQAImplJIT::shouldUse(XQAParams const& xqaParams, bool forConfigurePlugin) +bool DecoderXQAImplJIT::shouldUse(XQAParams const& umbrellaXQAParams, bool forConfigurePlugin) { - bool is_config_supported = supportConfig(xqaParams); if (forConfigurePlugin) { - return is_config_supported; + for (int beam_width = 1; beam_width <= umbrellaXQAParams.beam_width; ++beam_width) + { + XQAParams actualXQAParams = umbrellaXQAParams; + actualXQAParams.beam_width = beam_width; + if (supportConfig(actualXQAParams, forConfigurePlugin)) + { + return true; + } + } + return false; } else { - return is_config_supported && mayHavePerfGain(xqaParams); + auto const& xqaParams = umbrellaXQAParams; + return supportConfig(xqaParams, forConfigurePlugin) && mayHavePerfGain(xqaParams); } } @@ -115,29 +110,37 @@ jit::CubinObjKey DecoderXQAImplJIT::getCubinObjKeyFromXQAParams(XQAParams const& return {loadKey, runtimeKey}; } -void DecoderXQAImplJIT::prepare(XQAParams const& xqaParams) +void DecoderXQAImplJIT::prepareForActualXQAParams(XQAParams const& xqaParams) { - jit::CubinObjKey key = getCubinObjKeyFromXQAParams(xqaParams); + jit::CubinObjKey currentKey = getCubinObjKeyFromXQAParams(xqaParams); jit::CompileEngine compileEngine(mSM, xqaParams); auto registryGlobal = DecoderXQARunner::getResourceGlobal()->getCubinObjRegistry(); - jit::CubinObj* uninitializedCubin = registryGlobal->getCubin(key); - if (uninitializedCubin != nullptr) + + if (supportConfig(xqaParams, true)) { - // Inference time. Prepare for the inference. + jit::CubinObjKey key = getCubinObjKeyFromXQAParams(xqaParams); + registryGlobal->insertCubinIfNotExists(key, &compileEngine); if (mInitializedCubinObjRegistry.getCubin(key) == nullptr) { - // Make a copy and initialize it. + // Get an unintiailized cubin from registryGlobal, initialize it, then put it in + // mInitializedCubinRegistry. + jit::CubinObj* uninitializedCubin = registryGlobal->getCubin(key); jit::CubinObj initializedCubin = *uninitializedCubin; initializedCubin.initialize(); mInitializedCubinObjRegistry.insertCubin(key, std::move(initializedCubin)); } } - else +} + +void DecoderXQAImplJIT::prepare(XQAParams const& umbrellaXQAParams) +{ + for (int beam_width = 1; beam_width <= umbrellaXQAParams.beam_width; ++beam_width) { - // Engine-build time. Compile the cubin and place it into CubinObjRegistry. - registryGlobal->insertCubinIfNotExists(key, &compileEngine); + XQAParams actualXQAParams = umbrellaXQAParams; + actualXQAParams.beam_width = beam_width; + prepareForActualXQAParams(actualXQAParams); } } @@ -279,10 +282,9 @@ void DecoderXQAImplJIT::runImpl(XQAParams const& xqaParams, KVCacheBuffer const& } else { - bool const isGmmaKernel = (mSM == kSM_90 && xqaParams.kv_cache_data_type == XQADataType::DATA_TYPE_E4M3 - && xqaParams.beam_width == 1); + bool const isGMMAKernel = jit::supportConfigQGMMA(xqaParams, mSM, false); constexpr uint32_t kMAX_NB_KERNEL_PARAMS = 11; - uint32_t const maxNbKernelParams = (isGmmaKernel ? 11 : 10); + uint32_t const maxNbKernelParams = (isGMMAKernel ? 11 : 10); uint32_t idxNextParam = 0; void* kernelParams[kMAX_NB_KERNEL_PARAMS]; auto appendParam = [&](auto* p) mutable @@ -301,7 +303,7 @@ void DecoderXQAImplJIT::runImpl(XQAParams const& xqaParams, KVCacheBuffer const& appendParam(&launchParams.batch_size); appendParam(&launchParams.kv_scale_quant_orig); CUtensorMap tensorMap{}; - if (isGmmaKernel) + if (isGMMAKernel) { tensorMap = makeTensorMapForKVCache(mDriver, xqaParams, kv_cache_buffer); appendParam(&tensorMap); @@ -316,7 +318,7 @@ void DecoderXQAImplJIT::runImpl(XQAParams const& xqaParams, KVCacheBuffer const& } dim3 gridDim(multi_block, xqaParams.num_kv_heads, xqaParams.batch_size); - dim3 blockDim(128, 1, isGmmaKernel ? 3 : 2); + dim3 blockDim(128, 1, isGMMAKernel ? 3 : 2); cubinObj->launch(gridDim, blockDim, stream, kernelParams); } diff --git a/cpp/tensorrt_llm/kernels/decoderMaskedMultiheadAttention/decoderXQAImplJIT/decoderXQAImplJIT.h b/cpp/tensorrt_llm/kernels/decoderMaskedMultiheadAttention/decoderXQAImplJIT/decoderXQAImplJIT.h index ff10a44c6..d22926009 100644 --- a/cpp/tensorrt_llm/kernels/decoderMaskedMultiheadAttention/decoderXQAImplJIT/decoderXQAImplJIT.h +++ b/cpp/tensorrt_llm/kernels/decoderMaskedMultiheadAttention/decoderXQAImplJIT/decoderXQAImplJIT.h @@ -43,12 +43,13 @@ class DecoderXQAImplJIT : public DecoderXQAImpl private: std::shared_ptr mDriver; - void initSupportedConfigs(); //! Whether DecoderXQAImplJIT supports xqaParams. - bool supportConfig(XQAParams const& xqaParams) const; + bool supportConfig(XQAParams const& xqaParams, bool forConfigurePlugin) const; //! Whether DecoderXQAImplJIT has perf gain over the default (non-XQA-optimized) implementation. bool mayHavePerfGain(XQAParams const& xqaParams) const; + void prepareForActualXQAParams(XQAParams const& xqaParams); + template void runImpl(XQAParams const& xqaParams, KVCacheBuffer const& kv_cache_buffer, int multiprocessor_count, cudaStream_t const& stream); @@ -63,9 +64,6 @@ class DecoderXQAImplJIT : public DecoderXQAImpl jit::CubinObjRegistry mInitializedCubinObjRegistry; jit::CubinObjKey getCubinObjKeyFromXQAParams(XQAParams const& xqaParams) const; - - //! The first prototype just takes whatever available from the Precompiled cubins. - std::unordered_set mSupportedConfigs; }; } // namespace kernels diff --git a/cpp/tensorrt_llm/kernels/decoderMaskedMultiheadAttention/decoderXQAImplJIT/kernelUtils.cpp b/cpp/tensorrt_llm/kernels/decoderMaskedMultiheadAttention/decoderXQAImplJIT/kernelUtils.cpp new file mode 100644 index 000000000..36fc94a8d --- /dev/null +++ b/cpp/tensorrt_llm/kernels/decoderMaskedMultiheadAttention/decoderXQAImplJIT/kernelUtils.cpp @@ -0,0 +1,171 @@ +/* + * Copyright (c) 2020-2023, NVIDIA CORPORATION. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +#include "tensorrt_llm/kernels/decoderMaskedMultiheadAttention/decoderXQAImplJIT/kernelUtils.h" + +namespace tensorrt_llm +{ +namespace kernels +{ +namespace jit +{ + +namespace +{ + +template +bool contains(std::initializer_list const& c, T const& v) +{ + return std::find(c.begin(), c.end(), v) != c.end(); +} + +bool supportConfigCommon(XQAParams const& xqaParams, bool forConfigurePlugin) +{ + if (xqaParams.unidirectional != 1) + { + return false; + } + if (xqaParams.q_scaling != 1.0f) + { + return false; + } + if (xqaParams.mask_type != tensorrt_llm::kernels::AttentionMaskType::CAUSAL) + { + return false; + } + if (xqaParams.cross_attention) + { + return false; + } + if (xqaParams.cyclic_attention_window_size != xqaParams.max_attention_window_size) + { + return false; + } + if (xqaParams.position_shift_enabled || xqaParams.sink_token_length > 0) + { + return false; + } + if (!forConfigurePlugin && xqaParams.host_past_key_value_lengths == nullptr) + { + return false; + } + if (xqaParams.num_kv_heads == 0 || xqaParams.num_q_heads == xqaParams.num_kv_heads) + { + // Do not use XQA kernel for MHA. + return false; + } + if (xqaParams.multi_block_mode) + { + // TODO(minwei): Re-enable multi_block_mode once we figure out the issue. + return false; + } + if (!contains({PositionEmbeddingType::kROPE_GPTJ, PositionEmbeddingType::kROPE_GPT_NEOX, + PositionEmbeddingType::kLONG_ROPE}, + xqaParams.position_embedding_type)) + { + return false; + } + return true; +} + +} // anonymous namespace + +bool supportConfigQGMMA(XQAParams const& xqaParams, int SM, bool forConfigurePlugin) +{ + if (!supportConfigCommon(xqaParams, forConfigurePlugin)) + { + return false; + } + if (SM != kSM_90) + { + return false; + } + if (!contains({DATA_TYPE_FP16, DATA_TYPE_BF16}, xqaParams.data_type)) + { + return false; + } + if (xqaParams.kv_cache_data_type != DATA_TYPE_E4M3) + { + return false; + } + if (xqaParams.beam_width != 1) + { + return false; + } + if (xqaParams.head_size % 16 != 0 || xqaParams.head_size < 16 || xqaParams.head_size > 256) + { + return false; + } + if (xqaParams.num_kv_heads == 0 || xqaParams.num_q_heads % xqaParams.num_kv_heads != 0) + { + return false; + } + int32_t head_grp_size = xqaParams.num_q_heads / xqaParams.num_kv_heads; + if (head_grp_size * xqaParams.beam_width > 32) + { + return false; + } + if (xqaParams.paged_kv_cache && !contains({16, 32, 64, 128}, xqaParams.tokens_per_block)) + { + return false; + } + return true; +} + +bool supportConfigHMMA(XQAParams const& xqaParams, int SM, bool forConfigurePlugin) +{ + if (!supportConfigCommon(xqaParams, forConfigurePlugin)) + { + return false; + } + if (SM < kSM_80) + { + return false; + } + if (!contains({DATA_TYPE_FP16, DATA_TYPE_BF16}, xqaParams.data_type)) + { + return false; + } + if (!contains({DATA_TYPE_FP16, DATA_TYPE_BF16, DATA_TYPE_INT8, DATA_TYPE_E4M3}, xqaParams.kv_cache_data_type)) + { + return false; + } + if (xqaParams.beam_width != 1 && xqaParams.beam_width != 4) + { + return false; + } + if (xqaParams.head_size % 16 != 0 || xqaParams.head_size < 16 || xqaParams.head_size > 256) + { + return false; + } + if (xqaParams.num_kv_heads == 0 || xqaParams.num_q_heads % xqaParams.num_kv_heads != 0) + { + return false; + } + int32_t head_grp_size = xqaParams.num_q_heads / xqaParams.num_kv_heads; + if (head_grp_size * xqaParams.beam_width > 32) + { + return false; + } + if (xqaParams.paged_kv_cache && !contains({16, 32, 64, 128}, xqaParams.tokens_per_block)) + { + return false; + } + return true; +} + +} // namespace jit +} // namespace kernels +} // namespace tensorrt_llm diff --git a/cpp/tensorrt_llm/kernels/decoderMaskedMultiheadAttention/decoderXQAImplJIT/kernelUtils.h b/cpp/tensorrt_llm/kernels/decoderMaskedMultiheadAttention/decoderXQAImplJIT/kernelUtils.h new file mode 100644 index 000000000..c8744471d --- /dev/null +++ b/cpp/tensorrt_llm/kernels/decoderMaskedMultiheadAttention/decoderXQAImplJIT/kernelUtils.h @@ -0,0 +1,31 @@ +/* + * Copyright (c) 2020-2023, NVIDIA CORPORATION. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +#pragma once +#include "tensorrt_llm/kernels/decoderMaskedMultiheadAttention/xqaParams.h" + +namespace tensorrt_llm +{ +namespace kernels +{ +namespace jit +{ + +bool supportConfigQGMMA(XQAParams const& xqaParams, int SM, bool forConfigurePlugin); +bool supportConfigHMMA(XQAParams const& xqaParams, int SM, bool forConfigurePlugin); + +} // namespace jit +} // namespace kernels +} // namespace tensorrt_llm diff --git a/cpp/tensorrt_llm/kernels/decoderMaskedMultiheadAttention/decoderXQAImplJIT/nvrtcWrapper/aarch64-linux-gnu/libtensorrt_llm_nvrtc_wrapper.so b/cpp/tensorrt_llm/kernels/decoderMaskedMultiheadAttention/decoderXQAImplJIT/nvrtcWrapper/aarch64-linux-gnu/libtensorrt_llm_nvrtc_wrapper.so index 5c8c2ebf1..2f6765965 100644 --- a/cpp/tensorrt_llm/kernels/decoderMaskedMultiheadAttention/decoderXQAImplJIT/nvrtcWrapper/aarch64-linux-gnu/libtensorrt_llm_nvrtc_wrapper.so +++ b/cpp/tensorrt_llm/kernels/decoderMaskedMultiheadAttention/decoderXQAImplJIT/nvrtcWrapper/aarch64-linux-gnu/libtensorrt_llm_nvrtc_wrapper.so @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:8de0cd3bd46925e008f263b3f6c78c17f198578f74e23bc90661bec5a9acfbb1 -size 80250768 +oid sha256:947f76624c35015b35110b91511e6bf18d2428a41d0209f08a089361c3617541 +size 80309448 diff --git a/cpp/tensorrt_llm/kernels/decoderMaskedMultiheadAttention/decoderXQAImplJIT/nvrtcWrapper/aarch64-linux-gnu/version.txt b/cpp/tensorrt_llm/kernels/decoderMaskedMultiheadAttention/decoderXQAImplJIT/nvrtcWrapper/aarch64-linux-gnu/version.txt index a44f5448b..6a59d0857 100644 --- a/cpp/tensorrt_llm/kernels/decoderMaskedMultiheadAttention/decoderXQAImplJIT/nvrtcWrapper/aarch64-linux-gnu/version.txt +++ b/cpp/tensorrt_llm/kernels/decoderMaskedMultiheadAttention/decoderXQAImplJIT/nvrtcWrapper/aarch64-linux-gnu/version.txt @@ -1,2 +1,2 @@ -5b6c74ce66f62d2a58aa9cac16f11ad6 libtensorrt_llm_nvrtc_wrapper.so -265b039443334094026fbd8f396d52fe29c2d9d1 commit \ No newline at end of file +8b0f8deb35940359b39f876fc5e94e4f libtensorrt_llm_nvrtc_wrapper.so +0e1417f27d93de67940c1062cf230017cd8be5f1 commit \ No newline at end of file diff --git a/cpp/tensorrt_llm/kernels/decoderMaskedMultiheadAttention/decoderXQAImplJIT/nvrtcWrapper/include/nvrtcWrapper.h b/cpp/tensorrt_llm/kernels/decoderMaskedMultiheadAttention/decoderXQAImplJIT/nvrtcWrapper/include/nvrtcWrapper.h index 10cb9992c..932df23a4 100644 --- a/cpp/tensorrt_llm/kernels/decoderMaskedMultiheadAttention/decoderXQAImplJIT/nvrtcWrapper/include/nvrtcWrapper.h +++ b/cpp/tensorrt_llm/kernels/decoderMaskedMultiheadAttention/decoderXQAImplJIT/nvrtcWrapper/include/nvrtcWrapper.h @@ -35,6 +35,14 @@ extern "C" { #endif + typedef enum + { + // sm >= 80 + TLLM_XQA_JIT_HMMA = 0, + // sm == 90 + TLLM_XQA_JIT_QGMMA = 1 + } tllmXqaJitKernelType; + typedef struct { // Compute capability, e.g. 89. @@ -51,6 +59,8 @@ extern "C" // Actual type: tensorrt_llm::kernels::Data_type int data_type; int kv_cache_data_type; + + tllmXqaJitKernelType kernel_type; } tllmXqaJitContext; // tllmXqaJitProgram is an opaque handle for a program. diff --git a/cpp/tensorrt_llm/kernels/decoderMaskedMultiheadAttention/decoderXQAImplJIT/nvrtcWrapper/x86_64-linux-gnu/libtensorrt_llm_nvrtc_wrapper.so b/cpp/tensorrt_llm/kernels/decoderMaskedMultiheadAttention/decoderXQAImplJIT/nvrtcWrapper/x86_64-linux-gnu/libtensorrt_llm_nvrtc_wrapper.so index 967003354..5dc4a41aa 100755 --- a/cpp/tensorrt_llm/kernels/decoderMaskedMultiheadAttention/decoderXQAImplJIT/nvrtcWrapper/x86_64-linux-gnu/libtensorrt_llm_nvrtc_wrapper.so +++ b/cpp/tensorrt_llm/kernels/decoderMaskedMultiheadAttention/decoderXQAImplJIT/nvrtcWrapper/x86_64-linux-gnu/libtensorrt_llm_nvrtc_wrapper.so @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:bbf358364915d5b023a6d0574cde0f602c104d24efe0bf5c04eeee4610a2413e -size 83541760 +oid sha256:f70f4370d8c1440450bced06f055e0095dafeace3c16fd32f933c3ba666d0ed2 +size 83556480 diff --git a/cpp/tensorrt_llm/kernels/decoderMaskedMultiheadAttention/decoderXQAImplJIT/nvrtcWrapper/x86_64-windows-msvc/tensorrt_llm_nvrtc_wrapper.dll b/cpp/tensorrt_llm/kernels/decoderMaskedMultiheadAttention/decoderXQAImplJIT/nvrtcWrapper/x86_64-windows-msvc/tensorrt_llm_nvrtc_wrapper.dll index 8d3409955..f6b3237f1 100644 --- a/cpp/tensorrt_llm/kernels/decoderMaskedMultiheadAttention/decoderXQAImplJIT/nvrtcWrapper/x86_64-windows-msvc/tensorrt_llm_nvrtc_wrapper.dll +++ b/cpp/tensorrt_llm/kernels/decoderMaskedMultiheadAttention/decoderXQAImplJIT/nvrtcWrapper/x86_64-windows-msvc/tensorrt_llm_nvrtc_wrapper.dll @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:9f2f97eb5b4181917a47b6028a857d7a597ca93faa5846af42c4cb24797d7fa7 -size 1080832 +oid sha256:53746a0351295accb650f9e509303914ae8d8dc3c2605baf680f30cfc40d96f6 +size 1091072 diff --git a/cpp/tensorrt_llm/kernels/decoderMaskedMultiheadAttention/decoderXQAImplJIT/nvrtcWrapper/x86_64-windows-msvc/tensorrt_llm_nvrtc_wrapper.lib b/cpp/tensorrt_llm/kernels/decoderMaskedMultiheadAttention/decoderXQAImplJIT/nvrtcWrapper/x86_64-windows-msvc/tensorrt_llm_nvrtc_wrapper.lib index 9af1b79d9..94cf6f855 100644 --- a/cpp/tensorrt_llm/kernels/decoderMaskedMultiheadAttention/decoderXQAImplJIT/nvrtcWrapper/x86_64-windows-msvc/tensorrt_llm_nvrtc_wrapper.lib +++ b/cpp/tensorrt_llm/kernels/decoderMaskedMultiheadAttention/decoderXQAImplJIT/nvrtcWrapper/x86_64-windows-msvc/tensorrt_llm_nvrtc_wrapper.lib @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:4ca7c531980130dfd37c59132bbce8e90b821ecc31fa20d86726eec153bb016e +oid sha256:38d470721122f47b75e91b7967fa56cebcb48c8abcc4b3ddefe4f39c85d061f8 size 3488 diff --git a/cpp/tensorrt_llm/kernels/decoderMaskedMultiheadAttention/decoderXQAImplPrecompiled.cpp b/cpp/tensorrt_llm/kernels/decoderMaskedMultiheadAttention/decoderXQAImplPrecompiled.cpp index dfca83ddf..9b1ae1740 100644 --- a/cpp/tensorrt_llm/kernels/decoderMaskedMultiheadAttention/decoderXQAImplPrecompiled.cpp +++ b/cpp/tensorrt_llm/kernels/decoderMaskedMultiheadAttention/decoderXQAImplPrecompiled.cpp @@ -391,13 +391,82 @@ void DecoderXQAImplPrecompiled::runDispatchBuffer( #undef XQA_KERNEL_RUN -bool DecoderXQAImplPrecompiled::shouldUse(XQAParams const& xqaParams, bool /*forConfigurePlugin*/) +#define SUPPORT_RETURN_FALSE(X) \ + { \ + return false; \ + } + +bool DecoderXQAImplPrecompiled::shouldUse(XQAParams const& xqaParams, bool forConfigurePlugin) { + if (!(xqaParams.data_type == DATA_TYPE_FP16 || xqaParams.data_type == DATA_TYPE_BF16)) + { + SUPPORT_RETURN_FALSE("data type"); + } + bool const isGPTJBeam4Kernel = (xqaParams.head_size == 256 && xqaParams.beam_width == 4 && xqaParams.paged_kv_cache + && (xqaParams.tokens_per_block == 64 || xqaParams.tokens_per_block == 128)); + if (xqaParams.head_size != 128 && xqaParams.head_size != 256 && !isGPTJBeam4Kernel) + { + SUPPORT_RETURN_FALSE("head_size"); + } + if (xqaParams.unidirectional != 1) + { + SUPPORT_RETURN_FALSE("unidirectional"); + } + if (xqaParams.q_scaling != 1.0f) + { + SUPPORT_RETURN_FALSE("q_scaling"); + } + if (xqaParams.mask_type != tensorrt_llm::kernels::AttentionMaskType::CAUSAL) + { + SUPPORT_RETURN_FALSE("mask_type"); + } + if (xqaParams.cross_attention) + { + SUPPORT_RETURN_FALSE("cross_attention"); + } + // Only support 64/128 tokens per block. + if (xqaParams.paged_kv_cache && xqaParams.tokens_per_block != 64 && xqaParams.tokens_per_block != 128) + { + SUPPORT_RETURN_FALSE("paged_kv_cache"); + } + if (!forConfigurePlugin && xqaParams.host_past_key_value_lengths == nullptr) + { + SUPPORT_RETURN_FALSE("host_past_key_value_lengths"); + } + if (xqaParams.beam_width != 1 && !isGPTJBeam4Kernel) + { + SUPPORT_RETURN_FALSE("beam_width"); + } + if (xqaParams.cyclic_attention_window_size != xqaParams.max_attention_window_size) + { + SUPPORT_RETURN_FALSE("cyclic_attention_window_size != max_attention_window_size"); + } + if (xqaParams.position_shift_enabled || xqaParams.sink_token_length > 0) + { + SUPPORT_RETURN_FALSE("streaming-llm"); + } + + // OPTIMIZE: For the standard generation-phase MHA, there are still extra limitations. + // NOTE: Medusa mode = Multi_query_tokens > 1. + int const nbQHeads = xqaParams.num_q_heads; + int const nbKVHeads = xqaParams.num_kv_heads; + int const nbQHeadsPerKV = nbQHeads / nbKVHeads; + // MultiQueryTokens mode (Medusa mode) can support any nbQHeadsPerKV. + if (!xqaParams.multi_query_tokens) + { + if (nbQHeadsPerKV != 8 && nbQHeadsPerKV != 1) + { + SUPPORT_RETURN_FALSE("nbHeads"); + } + } + XQAKernelList const* xqa_kernel = getXQAKernels(mRunner->mDataType, tensorrt_llm::common::getSMVersion()); return xqa_kernel->supportConfig(xqaParams) && xqa_kernel->mayHavePerfGain(xqaParams, mRunner->mMultiProcessorCount); } +#undef SUPPORT_RETURN_FALSE + void DecoderXQAImplPrecompiled::prepare(XQAParams const&) { // Intentionally do nothing. diff --git a/cpp/tensorrt_llm/kernels/decoderMaskedMultiheadAttention/decoderXQARunner.cpp b/cpp/tensorrt_llm/kernels/decoderMaskedMultiheadAttention/decoderXQARunner.cpp index f5bf359f4..48f27c1f8 100644 --- a/cpp/tensorrt_llm/kernels/decoderMaskedMultiheadAttention/decoderXQARunner.cpp +++ b/cpp/tensorrt_llm/kernels/decoderMaskedMultiheadAttention/decoderXQARunner.cpp @@ -109,17 +109,21 @@ DecoderXQAImpl* DecoderXQARunner::getImplFromXQAParams(XQAParams const& xqaParam // non-medusa. return mPrecompiledImpl.get(); } - if (tensorrt_llm::common::getEnvEnableXQAJIT()) + + std::optional envEnableXQAJIT = tensorrt_llm::common::getEnvEnableXQAJIT(); + + if (envEnableXQAJIT.has_value()) { - return mJITImpl.get(); + return envEnableXQAJIT.value() ? mJITImpl.get() : mPrecompiledImpl.get(); } else { - return mPrecompiledImpl.get(); + // If no env var set, default to JIT impl. + return mJITImpl.get(); } } -bool DecoderXQARunner::shouldUseImpl(XQAParams const& xqa_params, bool for_configure_plugin) +bool DecoderXQARunner::shouldUse(XQAParams const& xqa_params, bool for_configure_plugin) { return getImplFromXQAParams(xqa_params)->shouldUse(xqa_params, for_configure_plugin); } diff --git a/cpp/tensorrt_llm/kernels/decoderMaskedMultiheadAttention/decoderXQARunner.h b/cpp/tensorrt_llm/kernels/decoderMaskedMultiheadAttention/decoderXQARunner.h index dfe53d903..5fb87e6aa 100644 --- a/cpp/tensorrt_llm/kernels/decoderMaskedMultiheadAttention/decoderXQARunner.h +++ b/cpp/tensorrt_llm/kernels/decoderMaskedMultiheadAttention/decoderXQARunner.h @@ -69,11 +69,6 @@ struct XQADispatchHelper<__nv_bfloat16, KVBlockArray> }; #endif -#define SUPPORT_RETURN_FALSE(X) \ - { \ - return false; \ - } - class DecoderXQARunner { public: @@ -86,72 +81,7 @@ class DecoderXQARunner * \param[in] forConfigurePlugin indicates whether this method is called in configurePlugin, or in * enqueueGeneration. */ - template - bool shouldUse(XQAParams const& xqaParams, bool forConfigurePlugin) - { - if (!(xqaParams.data_type == DATA_TYPE_FP16 || xqaParams.data_type == DATA_TYPE_BF16)) - { - SUPPORT_RETURN_FALSE("data type"); - } - bool const isGPTJBeam4Kernel = (xqaParams.head_size == 256 && xqaParams.beam_width == 4 - && xqaParams.paged_kv_cache && (xqaParams.tokens_per_block == 64 || xqaParams.tokens_per_block == 128)); - if (xqaParams.head_size != 128 && xqaParams.head_size != 256 && !isGPTJBeam4Kernel) - { - SUPPORT_RETURN_FALSE("head_size"); - } - if (xqaParams.unidirectional != 1) - { - SUPPORT_RETURN_FALSE("unidirectional"); - } - if (xqaParams.q_scaling != 1.0f) - { - SUPPORT_RETURN_FALSE("q_scaling"); - } - if (xqaParams.mask_type != tensorrt_llm::kernels::AttentionMaskType::CAUSAL) - { - SUPPORT_RETURN_FALSE("mask_type"); - } - if (xqaParams.cross_attention) - { - SUPPORT_RETURN_FALSE("cross_attention"); - } - // Only support 64/128 tokens per block. - if (xqaParams.paged_kv_cache && xqaParams.tokens_per_block != 64 && xqaParams.tokens_per_block != 128) - { - SUPPORT_RETURN_FALSE("paged_kv_cache"); - } - if (!forConfigurePlugin && xqaParams.host_past_key_value_lengths == nullptr) - { - SUPPORT_RETURN_FALSE("host_past_key_value_lengths"); - } - if (xqaParams.beam_width != 1 && !isGPTJBeam4Kernel) - { - SUPPORT_RETURN_FALSE("beam_width"); - } - if (xqaParams.cyclic_attention_window_size != xqaParams.max_attention_window_size) - { - SUPPORT_RETURN_FALSE("cyclic_attention_window_size != max_attention_window_size"); - } - if (xqaParams.position_shift_enabled || xqaParams.sink_token_length > 0) - { - SUPPORT_RETURN_FALSE("streaming-llm"); - } - - // OPTIMIZE: For the standard generation-phase MHA, there are still extra limitations. - // NOTE: Medusa mode = Multi_query_tokens > 1. - int const nbQHeads = xqaParams.num_q_heads; - int const nbKVHeads = xqaParams.num_kv_heads; - int const nbQHeadsPerKV = nbQHeads / nbKVHeads; - // MultiQueryTokens mode (Medusa mode) can support any nbQHeadsPerKV. - if (!xqaParams.multi_query_tokens) - { - if (nbQHeadsPerKV != 8 && nbQHeadsPerKV != 1) - { - SUPPORT_RETURN_FALSE("nbHeads"); - } - } - return shouldUseImpl(xqaParams, forConfigurePlugin); - } + bool shouldUse(XQAParams const& xqaParams, bool forConfigurePlugin); size_t getWorkspaceSize(int max_batch_beam_size, int max_num_tokens); @@ -171,7 +101,6 @@ class DecoderXQARunner static Resource* getResourceGlobal(); private: - bool shouldUseImpl(XQAParams const& xqa_params, bool for_configure_plugin); void prepareForRun(XQAParams const& xqa_params); template diff --git a/cpp/tensorrt_llm/kernels/decoderMaskedMultiheadAttention/instantiation/decoderMaskedMultiheadAttention160_bf16.cu b/cpp/tensorrt_llm/kernels/decoderMaskedMultiheadAttention/instantiation/decoderMaskedMultiheadAttention160_bf16.cu index 491eb7533..bd65335d7 100644 --- a/cpp/tensorrt_llm/kernels/decoderMaskedMultiheadAttention/instantiation/decoderMaskedMultiheadAttention160_bf16.cu +++ b/cpp/tensorrt_llm/kernels/decoderMaskedMultiheadAttention/instantiation/decoderMaskedMultiheadAttention160_bf16.cu @@ -33,7 +33,7 @@ namespace mmha #ifdef ENABLE_BF16 INSTANTIATE_MMHA_LAUNCHERS(__nv_bfloat16, kSizePerHead) #endif // ENABLE_BF16 -#endif +#endif // FAST_BUILD } // namespace mmha diff --git a/cpp/tensorrt_llm/kernels/decoderMaskedMultiheadAttention/tensorMapUtils.cpp b/cpp/tensorrt_llm/kernels/decoderMaskedMultiheadAttention/tensorMapUtils.cpp index c71293bfc..ca2a2c57d 100644 --- a/cpp/tensorrt_llm/kernels/decoderMaskedMultiheadAttention/tensorMapUtils.cpp +++ b/cpp/tensorrt_llm/kernels/decoderMaskedMultiheadAttention/tensorMapUtils.cpp @@ -59,7 +59,9 @@ CUtensorMap makeTensorMapForPagedKVCache(std::shared_ptr cons uint64_t const globalDims[] = {headElems, tokensPerPage, nbKHeads, 1U << 31}; uint32_t const headBytes = elemBytes * headElems; uint64_t const globalStrides[] = {headBytes, headBytes * tokensPerPage, headBytes * tokensPerPage * nbKHeads}; - uint32_t const partElems = std::min(headBytes, 128U) / elemBytes; + TLLM_CHECK(headElems <= 256); + uint32_t const paddedHeadElems = headElems <= 64 ? 64 : (headElems <= 128 ? 128 : 256); + uint32_t const partElems = std::min(elemBytes * paddedHeadElems, 128U) / elemBytes; uint32_t const boxDims[] = {partElems, std::min(tokensPerPage, nbTokensPerTile), 1, 1}; uint32_t const elemStrides[] = {1, 1, 1, 1}; @@ -69,7 +71,7 @@ CUtensorMap makeTensorMapForPagedKVCache(std::shared_ptr cons { case 128: return CU_TENSOR_MAP_SWIZZLE_128B; case 64: return CU_TENSOR_MAP_SWIZZLE_64B; - default: throw std::runtime_error("unsupported cache head size"); + default: TLLM_THROW("unsupported cache head size"); } }(); @@ -89,7 +91,9 @@ CUtensorMap makeTensorMapForContiguousKVCache(std::shared_ptr uint32_t elemBytes = getElemBytes(dataType); uint32_t const headBytes = elemBytes * headElems; uint64_t const globalStrides[] = {headBytes, headBytes * maxCacheLen, headBytes * maxCacheLen * nbKHeads}; - uint32_t const partElems = std::min(headBytes, 128U) / elemBytes; + TLLM_CHECK(headElems <= 256); + uint32_t const paddedHeadElems = headElems <= 64 ? 64 : (headElems <= 128 ? 128 : 256); + uint32_t const partElems = std::min(elemBytes * paddedHeadElems, 128U) / elemBytes; uint32_t const boxDims[] = {partElems, nbTokensPerTile, 1, 1}; uint32_t const elemStrides[] = {1, 1, 1, 1}; @@ -99,7 +103,7 @@ CUtensorMap makeTensorMapForContiguousKVCache(std::shared_ptr { case 128: return CU_TENSOR_MAP_SWIZZLE_128B; case 64: return CU_TENSOR_MAP_SWIZZLE_64B; - default: throw std::runtime_error("unsupported cache head size"); + default: TLLM_THROW("unsupported cache head size"); } }(); diff --git a/cpp/tensorrt_llm/kernels/decoderMaskedMultiheadAttentionUtils.h b/cpp/tensorrt_llm/kernels/decoderMaskedMultiheadAttentionUtils.h index 1dc6a0036..ac2bf2a6e 100644 --- a/cpp/tensorrt_llm/kernels/decoderMaskedMultiheadAttentionUtils.h +++ b/cpp/tensorrt_llm/kernels/decoderMaskedMultiheadAttentionUtils.h @@ -62,18 +62,18 @@ struct __align__(16) Float8_ #ifdef ENABLE_BF16 struct __align__(8) bf16_4_t { - __nv_bfloat162 x; - __nv_bfloat162 y; + __nv_bfloat162_raw x; + __nv_bfloat162_raw y; }; //////////////////////////////////////////////////////////////////////////////////////////////////// struct __align__(16) bf16_8_t { - __nv_bfloat162 x; - __nv_bfloat162 y; - __nv_bfloat162 z; - __nv_bfloat162 w; + __nv_bfloat162_raw x; + __nv_bfloat162_raw y; + __nv_bfloat162_raw z; + __nv_bfloat162_raw w; }; #endif @@ -4050,25 +4050,25 @@ template <> __device__ __inline__ void write_smem_transpose( bf16_4_t const& vec, __nv_bfloat16* smem, int transpose_idx, int smem_pitch) { - smem[transpose_idx] = vec.x.x; - smem[transpose_idx + 1] = vec.y.x; - smem[smem_pitch + transpose_idx] = vec.x.y; - smem[smem_pitch + transpose_idx + 1] = vec.y.y; + smem[transpose_idx] = __nv_bfloat162(vec.x).x; + smem[transpose_idx + 1] = __nv_bfloat162(vec.y).x; + smem[smem_pitch + transpose_idx] = __nv_bfloat162(vec.x).y; + smem[smem_pitch + transpose_idx + 1] = __nv_bfloat162(vec.y).y; } template <> __device__ __inline__ void write_smem_transpose( bf16_8_t const& vec, __nv_bfloat16* smem, int transpose_idx, int smem_pitch) { - smem[transpose_idx] = vec.x.x; - smem[transpose_idx + 1] = vec.y.x; - smem[transpose_idx + 2] = vec.z.x; - smem[transpose_idx + 3] = vec.w.x; + smem[transpose_idx] = __nv_bfloat162(vec.x).x; + smem[transpose_idx + 1] = __nv_bfloat162(vec.y).x; + smem[transpose_idx + 2] = __nv_bfloat162(vec.z).x; + smem[transpose_idx + 3] = __nv_bfloat162(vec.w).x; - smem[smem_pitch + transpose_idx] = vec.x.y; - smem[smem_pitch + transpose_idx + 1] = vec.y.y; - smem[smem_pitch + transpose_idx + 2] = vec.z.y; - smem[smem_pitch + transpose_idx + 3] = vec.w.y; + smem[smem_pitch + transpose_idx] = __nv_bfloat162(vec.x).y; + smem[smem_pitch + transpose_idx + 1] = __nv_bfloat162(vec.y).y; + smem[smem_pitch + transpose_idx + 2] = __nv_bfloat162(vec.z).y; + smem[smem_pitch + transpose_idx + 3] = __nv_bfloat162(vec.w).y; } #endif diff --git a/cpp/tensorrt_llm/kernels/decodingKernels.cu b/cpp/tensorrt_llm/kernels/decodingKernels.cu index e27534d62..8b795f185 100644 --- a/cpp/tensorrt_llm/kernels/decodingKernels.cu +++ b/cpp/tensorrt_llm/kernels/decodingKernels.cu @@ -482,11 +482,12 @@ __global__ void finalizeKernel(BeamHypotheses bh) void invokeFinalize(BeamHypotheses& bh, cudaStream_t stream) { - TLLM_LOG_DEBUG("%s %s start", __FILE__, __PRETTY_FUNCTION__); + TLLM_LOG_TRACE("%s %s start", __FILE__, __PRETTY_FUNCTION__); int const nBM = bh.nBeamWidth; size_t const smem_size = sizeof(int) * nBM * 2 + sizeof(float) * nBM * 2; finalizeKernel<<>>(bh); + TLLM_LOG_TRACE("%s %s stop", __FILE__, __PRETTY_FUNCTION__); } __global__ void initializeOutput(TokenIdType* finalOutputIds, TokenIdType const* endIds, SizeType32 const nMaxSeqLen) diff --git a/cpp/tensorrt_llm/kernels/mixtureOfExperts/moe_kernels.h b/cpp/tensorrt_llm/kernels/mixtureOfExperts/moe_kernels.h index 675398448..98b425dbf 100644 --- a/cpp/tensorrt_llm/kernels/mixtureOfExperts/moe_kernels.h +++ b/cpp/tensorrt_llm/kernels/mixtureOfExperts/moe_kernels.h @@ -87,6 +87,23 @@ struct MOEParallelismConfig int ep_size = 1; int ep_rank = 0; + MOEParallelismConfig() = default; + + MOEParallelismConfig(int tp_size, int tp_rank, int ep_size, int ep_rank) + : tp_size(tp_size) + , tp_rank(tp_rank) + , ep_size(ep_size) + , ep_rank(ep_rank) + { + // Do some basic sanity checks + TLLM_CHECK(tp_rank < tp_size); + TLLM_CHECK(tp_rank >= 0); + TLLM_CHECK(tp_size >= 1); + TLLM_CHECK(ep_rank < ep_size); + TLLM_CHECK(ep_rank >= 0); + TLLM_CHECK(ep_size >= 1); + } + bool operator==(MOEParallelismConfig const& other) const { return tp_size == other.tp_size && tp_rank == other.tp_rank && ep_size == other.ep_size diff --git a/cpp/tensorrt_llm/kernels/samplingAirTopPKernels.cu b/cpp/tensorrt_llm/kernels/samplingAirTopPKernels.cu index 43e994b55..45b844d2c 100644 --- a/cpp/tensorrt_llm/kernels/samplingAirTopPKernels.cu +++ b/cpp/tensorrt_llm/kernels/samplingAirTopPKernels.cu @@ -27,6 +27,9 @@ #include "tensorrt_llm/kernels/samplingTopPKernels.h" #include +#include +#include + #include #include @@ -74,8 +77,7 @@ struct alignas(128) Counter alignas(128) IdxT filterCnt; // For a row inside a batch, we may launch multiple thread blocks. This counter is - // used to determine if the current block is the last running block. If so, this block - // will execute scan() and chooseBucket(). + // used to determine if the current block is the last running block. alignas(128) uint32_t finishedBlockCnt; }; @@ -698,37 +700,6 @@ __device__ void scan(IdxT volatile* histogram, IdxT* histogramOut) } } -/** - * Calculate in which bucket the k-th value will fall - * (steps 3 in `airTopPSampling` description) - */ -template -__device__ void chooseBucket( - Counter* counter, AccT const* histogram, IdxT const* countHistogram, AccT const sum, int const pass) -{ - int constexpr numBuckets = calcNumBuckets(); - for (int i = threadIdx.x; i < numBuckets; i += blockDim.x) - { - AccT prev = (i == 0) ? 0 : histogram[i - 1]; - AccT cur = histogram[i]; - - // one and only one thread will satisfy this condition, so counter is - // written by only one thread - // Add strict check for negetive cases. - if ((sum > 0 && prev < sum && cur >= sum) || (sum <= 0 && prev == 0 && cur != 0)) - { - if (countHistogram[i]) // Only check meaningful ones - { - counter->sum = sum - prev; // how many values still are there to find - counter->len = countHistogram[i]; // cur - prev; // number of values in next pass - typename cub::Traits::UnsignedBits bucket = i; - int startBit = calcStartBit(pass); - counter->kthValueBits |= bucket << startBit; - } - } - } -} - /** * Computes sequenceLength, finished state, outputLogProbs, and cumLogProbs. */ @@ -1135,44 +1106,89 @@ __global__ void airTopPSampling(Counter* counters, HisT* histogra } } } - __shared__ IdxT maxBucket; - if (pass > 0) - { - // Avoid the scenario where currentSum is larger than the meaningful maximum prefix sum. - // This situation happens because these two values are calculted in different ways. - // So the precision loss during the calculation is also different. - if (threadIdx.x == 0) + // To avoid the error related to the prefix sum from cub, we find the bucket sequentially. + int constexpr WARP_SIZE = 32; + int constexpr WARP_COUNT = numBuckets / WARP_SIZE; + namespace cg = cooperative_groups; + cg::thread_block block = cg::this_thread_block(); + cg::thread_block_tile<32> warp = cg::tiled_partition<32>(block); + AccT* histPtr = isDeterministic ? histValueSmem : reinterpret_cast(histogram); + __shared__ AccT warpSum[WARP_COUNT]; + __shared__ cuda::atomic blockSum; + if constexpr (BitsPerPass != 11) + { + for (int i = threadIdx.x; i < numBuckets; i += BlockSize) { - maxBucket = 0; + warpSum[i] = 0; } __syncthreads(); - for (int i = threadIdx.x; i < numBuckets; i += blockDim.x) + } + + // Acquire the summation of each 32 buckets + for (int i = threadIdx.x; i < numBuckets; i += BlockSize) + { + reduce_store_async(warp, warpSum + i / WARP_SIZE, histPtr[i], cg::plus{}); + } + __syncthreads(); + + // Acquire the summation of all the 2048 buckets + if (threadIdx.x < WARP_SIZE) + { + reduce_store_async(warp, blockSum, warpSum[threadIdx.x], cg::plus{}); + if constexpr (BitsPerPass == 11) { - if (countHistogram[i]) - { - atomicMax(&maxBucket, i); - } + reduce_update_async(warp, blockSum, warpSum[threadIdx.x + WARP_SIZE], cg::plus{}); } - __syncthreads(); } - - scan( - isDeterministic ? histValueSmem : reinterpret_cast(histogram), histValueSmem); __syncthreads(); + + // Update currentSum if (pass == 0) { - currentSum = histValueSmem[numBuckets - 1] * counter->p; + currentSum = blockSum * counter->p; } - else + + if (threadIdx.x == 0) { - if (currentSum > histValueSmem[maxBucket]) + AccT prev = 0; + + // Add 32 elements each step + int iStep = 0; + int targetStep = 0; + for (; iStep < WARP_COUNT; iStep++) { - currentSum = histValueSmem[maxBucket]; + if (warpSum[iStep]) + { + targetStep = iStep; + if ((prev + warpSum[iStep]) >= currentSum) + { + break; + } + prev += warpSum[iStep]; + } + } + + int targetIdx = 0; + for (int i = targetStep * WARP_SIZE; i < numBuckets; i++) + { + if (countHistogram[i]) + { + targetIdx = i; + if ((prev + histPtr[i]) >= currentSum) + { + break; + } + prev += histPtr[i]; + } } - } - chooseBucket(counter, histValueSmem, countHistogram, currentSum, pass); + counter->sum = currentSum - prev; // how many values still are there to find + counter->len = countHistogram[targetIdx]; // cur - prev; // number of values in next pass + typename cub::Traits::UnsignedBits bucket = targetIdx; + int startBit = calcStartBit(pass); + counter->kthValueBits |= bucket << startBit; + } __syncthreads(); int constexpr numPasses = calcNumPasses(); diff --git a/cpp/tensorrt_llm/kernels/speculativeDecoding/medusaDecodingKernels.cu b/cpp/tensorrt_llm/kernels/speculativeDecoding/medusaDecodingKernels.cu index a3a32ca7d..58be5afdf 100644 --- a/cpp/tensorrt_llm/kernels/speculativeDecoding/medusaDecodingKernels.cu +++ b/cpp/tensorrt_llm/kernels/speculativeDecoding/medusaDecodingKernels.cu @@ -81,7 +81,8 @@ __global__ void acceptDraftTokensByIdsWithPaths(TokenIdType* outputIds, TokenIdT // Break if path terminates if (tokenId == -1) { - acceptedLength = ti; + hasEnd = targetToken == endId; // check if last token is EOS when path terminates. + acceptedLength = hasEnd ? ti - 1 : ti; break; } auto const targetTokenIdx = batchSlot * maxDecodingTokens + tokenId; diff --git a/cpp/tensorrt_llm/kernels/weightOnlyBatchedGemv/fp8Gemm.cu b/cpp/tensorrt_llm/kernels/weightOnlyBatchedGemv/fp8Gemm.cu index 28fd1997f..f42200187 100644 --- a/cpp/tensorrt_llm/kernels/weightOnlyBatchedGemv/fp8Gemm.cu +++ b/cpp/tensorrt_llm/kernels/weightOnlyBatchedGemv/fp8Gemm.cu @@ -38,8 +38,7 @@ __global__ void fp8Gemm(InputType const* __restrict__ act, InputType const* __re float acc[TILE_M * TILE_N]; static_assert(kStepK % 4 == 0); - using CvtInputType - = std::conditional_t, cutlass::float_e4m3_t, cutlass::float_e5m2_t>; + using CvtInputType = typename tensorrt_llm::kernels::cutlass_kernels::TllmToCutlassTypeAdapter::type; using Converter = cutlass::NumericArrayConverter; using CvtSrcType = typename Converter::source_type; using CvtResType = typename Converter::result_type; @@ -55,8 +54,7 @@ __global__ void fp8Gemm(InputType const* __restrict__ act, InputType const* __re output += tileIdM * n + tileIdN; for (SizeType32 idxK = tid * kStepK; idxK < k; idxK += kTileK) { -#pragma unroll - for (SizeType32 i = 0; i < TILE_N; ++i) + for (SizeType32 i = 0; i < TILE_N && i + tileIdN < n; ++i) { auto tile_w_quantized = reinterpret_cast(weight + i * k + idxK)[0]; #pragma unroll @@ -109,8 +107,7 @@ __global__ void fp8Gemm(InputType const* __restrict__ act, InputType const* __re } } __syncthreads(); -#pragma unroll - for (SizeType32 ii = tid; ii < TILE_M * TILE_N; ii += BLOCK_SIZE) + for (SizeType32 ii = tid; ii < TILE_M * TILE_N && ii % TILE_N + tileIdN < n; ii += BLOCK_SIZE) { SizeType32 mid = ii / TILE_N, nid = ii % TILE_N; float val = 0; @@ -124,34 +121,135 @@ __global__ void fp8Gemm(InputType const* __restrict__ act, InputType const* __re } template -void fp8GemmKernel(Params& params, cudaStream_t stream) +void fp8GemmKernel(Params const& params, cudaStream_t stream) { dim3 block(BLOCK_SIZE); - dim3 grid(params.m / TILE_M, params.n / TILE_N); + dim3 grid(params.m / TILE_M, (params.n + TILE_N - 1) / TILE_N); fp8Gemm<<>>( reinterpret_cast(params.act), reinterpret_cast(params.weight), params.alpha, reinterpret_cast(params.output), params.m, params.n, params.k); } +template +bool fp8GemmTemplateCaller(Params const& params, cudaStream_t stream) +{ + constexpr int fp8GemmTemplateMaxM = 16; + if (params.m == TILE_M) + { + fp8GemmKernel(params, stream); + return true; + } + if constexpr (TILE_M < fp8GemmTemplateMaxM) + { + return fp8GemmTemplateCaller(params, stream); + } + return false; +} + template -void fp8GemmLauncher(Params& params, cudaStream_t stream) +bool fp8GemmLauncher(Params const& params, cudaStream_t stream) { -#define DISPATCH(TargetM, TILE_M, TILE_N, BLOCK_SIZE) \ - if (params.m == TargetM) \ - { \ - fp8GemmKernel(params, stream); \ - return; \ + return fp8GemmTemplateCaller(params, stream); +} + +bool fp8GemmDispatcher(Params const& params, cudaStream_t stream) +{ + bool dispatched = true; + if (params.inputType == nvinfer1::DataType::kFP8) + { + if (params.k % 16 != 0) + { + // Expect k % 16 == 0 for 128 bits alignment + dispatched = false; + } + else if (params.outputType == nvinfer1::DataType::kHALF) + { + dispatched = tensorrt_llm::kernels::fp8_gemm::fp8GemmLauncher<__nv_fp8_e4m3, half>(params, stream); + } + else if (params.outputType == nvinfer1::DataType::kBF16) + { + dispatched = tensorrt_llm::kernels::fp8_gemm::fp8GemmLauncher<__nv_fp8_e4m3, __nv_bfloat16>(params, stream); + } + else if (params.outputType == nvinfer1::DataType::kFLOAT) + { + dispatched = tensorrt_llm::kernels::fp8_gemm::fp8GemmLauncher<__nv_fp8_e4m3, float>(params, stream); + } + else + { + dispatched = false; + } + } + else if (params.inputType == nvinfer1::DataType::kHALF) + { + if (params.k % 8 != 0) + { + // Expect k % 8 == 0 for 128 bits alignment + dispatched = false; + } + else if (params.outputType == nvinfer1::DataType::kHALF) + { + dispatched = tensorrt_llm::kernels::fp8_gemm::fp8GemmLauncher(params, stream); + } + else if (params.outputType == nvinfer1::DataType::kFLOAT) + { + dispatched = tensorrt_llm::kernels::fp8_gemm::fp8GemmLauncher(params, stream); + } + else + { + dispatched = false; + } + } + else if (params.inputType == nvinfer1::DataType::kBF16) + { + if (params.k % 8 != 0) + { + // Expect k % 8 == 0 for 128 bits alignment + dispatched = false; + } + else if (params.outputType == nvinfer1::DataType::kBF16) + { + dispatched = tensorrt_llm::kernels::fp8_gemm::fp8GemmLauncher<__nv_bfloat16, __nv_bfloat16>(params, stream); + } + else if (params.outputType == nvinfer1::DataType::kFLOAT) + { + dispatched = tensorrt_llm::kernels::fp8_gemm::fp8GemmLauncher<__nv_bfloat16, float>(params, stream); + } + else + { + dispatched = false; + } + } + else if (params.inputType == nvinfer1::DataType::kFLOAT) + { + if (params.k % 4 != 0) + { + // Expect k % 4 == 0 for 128 bits alignment + dispatched = false; + } + else if (params.outputType == nvinfer1::DataType::kFLOAT) + { + dispatched = tensorrt_llm::kernels::fp8_gemm::fp8GemmLauncher(params, stream); + } + else + { + dispatched = false; + } + } + else + { + dispatched = false; + } + + if (!dispatched) + { + TLLM_LOG_DEBUG( + "tensorrt_llm::kernels::fp8_gemm::fp8GemmDispatcher failed to dispatch: inputType=%d, outputType=%d, m=%d, " + "n=%d, k=%d", + params.inputType, params.outputType, params.m, params.n, params.k); } - DISPATCH(1, 1, 2, 128); - DISPATCH(2, 2, 2, 128); - DISPATCH(3, 3, 2, 128); - DISPATCH(4, 4, 2, 128); -#undef DISPATCH + return dispatched; } -template void fp8GemmLauncher<__nv_fp8_e4m3, float>(Params& params, cudaStream_t stream); -template void fp8GemmLauncher<__nv_fp8_e4m3, half>(Params& params, cudaStream_t stream); -template void fp8GemmLauncher<__nv_fp8_e4m3, __nv_bfloat16>(Params& params, cudaStream_t stream); } // namespace fp8_gemm } // namespace kernels } // namespace tensorrt_llm diff --git a/cpp/tensorrt_llm/kernels/weightOnlyBatchedGemv/fp8Gemm.h b/cpp/tensorrt_llm/kernels/weightOnlyBatchedGemv/fp8Gemm.h index 1f7f50c97..ab7131034 100644 --- a/cpp/tensorrt_llm/kernels/weightOnlyBatchedGemv/fp8Gemm.h +++ b/cpp/tensorrt_llm/kernels/weightOnlyBatchedGemv/fp8Gemm.h @@ -15,8 +15,14 @@ */ #pragma once +#include "tensorrt_llm/common/assert.h" +#include "tensorrt_llm/common/logger.h" #include "tensorrt_llm/common/quantization.h" +#include "tensorrt_llm/kernels/cutlass_kernels/cutlass_type_conversion.h" #include "tensorrt_llm/runtime/common.h" + +#include + #include #include #include @@ -43,9 +49,12 @@ struct Params void* output; SizeType32 m, n, k; tensorrt_llm::common::QuantMode quantMode; + nvinfer1::DataType inputType; + nvinfer1::DataType outputType; Params(void const* _act, void const* _weight, float _alpha, void* _output, SizeType32 _m, SizeType32 _n, - SizeType32 _k, tensorrt_llm::common::QuantMode _quant_mode) + SizeType32 _k, tensorrt_llm::common::QuantMode _quant_mode, nvinfer1::DataType _inputType, + nvinfer1::DataType _outputType) : act(_act) , weight(_weight) , alpha(_alpha) @@ -54,12 +63,13 @@ struct Params , n(_n) , k(_k) , quantMode(_quant_mode) + , inputType(_inputType) + , outputType(_outputType) { } }; -template -void fp8GemmLauncher(Params& params, cudaStream_t stream); +bool fp8GemmDispatcher(Params const& params, cudaStream_t stream); } // namespace fp8_gemm } // namespace kernels } // namespace tensorrt_llm diff --git a/cpp/tensorrt_llm/layers/beamSearchLayer.cu b/cpp/tensorrt_llm/layers/beamSearchLayer.cu index 5aaf77aad..c0855349e 100644 --- a/cpp/tensorrt_llm/layers/beamSearchLayer.cu +++ b/cpp/tensorrt_llm/layers/beamSearchLayer.cu @@ -18,7 +18,6 @@ #include "tensorrt_llm/layers/beamSearchLayer.h" #include "tensorrt_llm/layers/defaultDecodingParams.h" #include "tensorrt_llm/layers/layerUtils.h" - #include using namespace tensorrt_llm::common; @@ -35,6 +34,15 @@ BeamSearchLayer::BeamSearchLayer( , mVocabSizePadded(decoderDomain.getVocabSizePadded()) { TLLM_LOG_TRACE(__PRETTY_FUNCTION__); + + mDiversityRateHost.resize(mDecoderDomain.getBatchSize()); + mLengthPenaltyHost.resize(mDecoderDomain.getBatchSize()); + mEarlyStoppingHost.resize(mDecoderDomain.getBatchSize()); + allocateBuffer(mDecoderDomain.getBatchSize(), mDecoderDomain.getBeamWidth()); + + TLLM_CHECK_WITH_INFO(mDecoderDomain.getBeamWidth() <= nMaxBeamWidth, + std::string("Beam width is larger than the maximum supported (" + std::to_string(mDecoderDomain.getBeamWidth()) + + " > " + std::to_string(nMaxBeamWidth) + ").")); } template @@ -48,29 +56,28 @@ void BeamSearchLayer::setup(runtime::SizeType32 const batchSize, runtime::Siz runtime::SizeType32 const* batchSlots, std::shared_ptr const& baseSetupParams) { TLLM_LOG_TRACE("%s start", __PRETTY_FUNCTION__); - TLLM_CHECK_WITH_INFO( - beamWidth <= nMaxBeamWidth, std::string("Beam width is larger than the maximum supported (64).")); + TLLM_CHECK_WITH_INFO(beamWidth <= mDecoderDomain.getBeamWidth(), + std::string("Beam width is larger than the constructed for (" + std::to_string(beamWidth) + " > " + + std::to_string(mDecoderDomain.getBeamWidth()) + ").")); auto setupParams = std::dynamic_pointer_cast(baseSetupParams); - mDiversityRateHost.resize(batchSize); - mLengthPenaltyHost.resize(batchSize); - mEarlyStoppingHost.resize(batchSize); - allocateBuffer(batchSize, beamWidth); - auto constexpr fltMax = std::numeric_limits::max(); auto constexpr fltMin = std::numeric_limits::lowest(); auto constexpr fltEpsilon = std::numeric_limits::epsilon(); - FillBuffers const fillBuffers{batchSize, batchSize, mStream}; + std::vector batchSlotsVec(batchSize); + std::iota(batchSlotsVec.begin(), batchSlotsVec.end(), 0); + auto batchSlotsHost = batchSlots ? batchSlots : batchSlotsVec.data(); + + FillBuffers const fillBuffers{batchSize, mDecoderDomain.getBatchSize(), mStream}; fillBuffers(setupParams->beamSearchDiversityRate, DefaultDecodingParams::getBeamSearchDiversity(), - mDiversityRateHost, mDiversityRateDevice, (int*) nullptr, std::make_pair(-fltEpsilon, fltMax), - "diveristy rate"); + mDiversityRateHost, mDiversityRateDevice, batchSlotsHost, std::make_pair(-fltEpsilon, fltMax), + "diversity rate"); fillBuffers(setupParams->lengthPenalty, DefaultDecodingParams::getLengthPenalty(), mLengthPenaltyHost, - mLengthPenaltyDevice, (int*) nullptr, std::make_pair(fltMin, fltMax), "length penalty"); + mLengthPenaltyDevice, batchSlotsHost, std::make_pair(fltMin, fltMax), "length penalty"); fillBuffers(setupParams->earlyStopping, DefaultDecodingParams::getEarlyStopping(), mEarlyStoppingHost, - mEarlyStoppingDevice, (int*) nullptr, std::make_pair(fltMin, fltMax), "early stopping"); - mHasDiffRuntimeArgs = setupParams->hasDiffRuntimeArgs; + mEarlyStoppingDevice, batchSlotsHost, std::make_pair(0, std::numeric_limits::max()), "early stopping"); TLLM_LOG_TRACE("%s stop", __PRETTY_FUNCTION__); } @@ -80,54 +87,68 @@ __global__ void updateCacheIndirectionKernel( { // Update indirections from steps `bh.inputLength[indexBatchBeam]` to step `sequenceLengths[indexBatchBeam]` int const step = threadIdx.x + blockIdx.x * blockDim.x; - int const indexBatchBeam = blockIdx.y; - int const nBS{bh.nBatchSize}; int const nBM{bh.nBeamWidth}; int const nMSL{bh.nMaxSeqLen}; - int const indexBatch = indexBatchBeam / nBM; - int const indexBeam = indexBatchBeam % nBM; + int const indexBatch = blockIdx.y; + int const batchSlot = bh.batchSlots ? bh.batchSlots[indexBatch] : indexBatch; + int const indexBeam = blockIdx.z; + int const indexBatchBeam = batchSlot * nBM + indexBeam; int const lastStep{bh.sequenceLengths[indexBatchBeam] - 1}; // the sequenceLengths is updated, need to minus 1 // Return early when the indexBatchBeam or step is out of the bound // No update for the indices of context part since KV Cache is shared - if (indexBatchBeam >= nBM * nBS || step >= nMSL || step < bh.inputLengths[indexBatchBeam] - || step < (nMSL - nMaxAttentionWindow) || bh.finished[indexBatchBeam].isFinished()) + if (step >= nMSL || step < bh.inputLengths[indexBatchBeam] || step < (nMSL - nMaxAttentionWindow) + || bh.finished[indexBatchBeam].isFinished()) { return; } // Keep all past tokens by parentIdsPtr - int const indexBeamSrc = bh.parentIdsPtr[indexBatch][indexBeam * nMSL + lastStep]; + int const indexBeamSrc = bh.parentIdsPtr[batchSlot][indexBeam * nMSL + lastStep]; int const stepCirc = (step >= nSinkTokenLength) ? nSinkTokenLength + (step - nSinkTokenLength) % (nMaxAttentionWindow - nSinkTokenLength) : step; // Consider cyclic kv cache for the indir tables - uint32_t const tgtOffset = indexBatch * nBM * nMaxAttentionWindow + indexBeam * nMaxAttentionWindow + stepCirc; - uint32_t const srcOffset = indexBatch * nBM * nMaxAttentionWindow + indexBeamSrc * nMaxAttentionWindow + stepCirc; + uint32_t const tgtOffset = batchSlot * nBM * nMaxAttentionWindow + indexBeam * nMaxAttentionWindow + stepCirc; + uint32_t const srcOffset = batchSlot * nBM * nMaxAttentionWindow + indexBeamSrc * nMaxAttentionWindow + stepCirc; tgtCI[tgtOffset] = (step == lastStep) ? indexBeam : srcCI[srcOffset]; } template -void BeamSearchLayer::forwardAsyncSingleRequest( +void BeamSearchLayer::forwardAsync( std::shared_ptr const& baseOutputs, std::shared_ptr const& baseInputs) { TLLM_LOG_TRACE("%s start", __PRETTY_FUNCTION__); - auto ip = std::dynamic_pointer_cast(baseInputs); + auto ip = std::dynamic_pointer_cast(baseInputs); auto op = std::dynamic_pointer_cast(baseOutputs); + auto const localDecoderDomain = getLocalDecoderDomain(ip, mDecoderDomain); + TLLM_CHECK_WITH_INFO(localDecoderDomain.getBeamWidth() > 1, + "Decoding mode is beam search, but beamWidth <= 1 (%d <= 1)", localDecoderDomain.getBeamWidth()); + TLLM_CHECK_WITH_INFO(ip->srcCacheIndirection.has_value(), "srcCacheIndirection is mandatory in beam search."); + TLLM_CHECK_WITH_INFO(op->parentIds.has_value(), "parentIds tensor is mandatory in beam search."); + TLLM_CHECK_WITH_INFO(op->finished.has_value(), "finished tensor is mandatory in beam search."); + TLLM_CHECK_WITH_INFO(op->cumLogProbs.has_value(), "cumLogProbs tensor is mandatory in beam search."); TLLM_CHECK_WITH_INFO(op->beamHypotheses, std::string("Output BeamHypotheses is not set.")); TLLM_CHECK_WITH_INFO(op->sequenceLength->template getPtr() != nullptr || mLengthPenaltyDevice == nullptr, std::string("Current sequence lengths must be set for length penalty computation.")); TLLM_CHECK_WITH_INFO(ip->ite == 0, "Pipeline Parallelism is not supported yet !"); - BeamHypotheses& bh{*op->beamHypotheses}; - // bh's members already initialized in op: *CBA, batchDones + BeamHypotheses bh; // bh's members not used in function: outputIds, logProbs, outputIdsUnfinish, parentIdsUnfinish + bh.outputIdsCBA = op->beamHypotheses->outputIdsCBA; + bh.logProbsCBA = op->beamHypotheses->logProbsCBA; + bh.sequenceLengthsCBA = op->beamHypotheses->sequenceLengthsCBA; + bh.cumLogProbsCBA = op->beamHypotheses->cumLogProbsCBA; + bh.normedScoresCBA = op->beamHypotheses->normedScoresCBA; + bh.numBeamsCBA = op->beamHypotheses->numBeamsCBA; + bh.minNormedScoresCBA = op->beamHypotheses->minNormedScoresCBA; + bh.batchDones = op->beamHypotheses->batchDones; bh.nMaxBatchSize = static_cast(op->outputIdsPtr.shape[0]); bh.nBatchSize = ip->localBatchSize; + bh.batchSlots = ip->batchSlots ? ip->batchSlots->template getPtr() : nullptr; bh.nBeamWidth = static_cast(op->outputIdsPtr.shape[1]); - bh.nIte = ip->ite; bh.nMaxSeqLen = static_cast(op->outputIdsPtr.shape[2]); bh.nVocabSize = mVocabSizePadded; bh.diversityRates = mDiversityRateDevice; @@ -135,14 +156,14 @@ void BeamSearchLayer::forwardAsyncSingleRequest( bh.earlyStoppings = mEarlyStoppingDevice; bh.inputLengths = ip->inputLengths->template getPtr(); bh.endIds = ip->endIds.template getPtr(); - bh.logProbsTiled = (op->outputLogProbs) ? op->outputLogProbs->template getPtr() : nullptr; + bh.logProbsTiled = (op->outputLogProbsTiled) ? op->outputLogProbsTiled->template getPtr() : nullptr; bh.sequenceLengths = op->sequenceLength->template getPtr(); bh.cumLogProbs = op->cumLogProbs->template getPtr(); bh.finished = reinterpret_cast(op->finished->template getPtr()); bh.outputIdsPtr = op->outputIdsPtr.template getPtr(); bh.parentIdsPtr = op->parentIdsPtr.template getPtr(); - T const* logits = ip->logits.template getPtr(); + T const* logits = ip->logits->template getPtr(); T const* bias = static_cast(nullptr); TLLM_CHECK_WITH_INFO(mWorkspaceSize >= 2 * bh.nBatchSize * bh.nBeamWidth * bh.nBeamWidth * 2, fmtstr("Workspace size (%lu) is not enough for topk softmax required (%lu).", (uint64_t) mWorkspaceSize, @@ -154,8 +175,8 @@ void BeamSearchLayer::forwardAsyncSingleRequest( if (bh.nBeamWidth > 1) { auto tgtCI = op->tgtCacheIndirection.template getPtr(); - auto srcCI = ip->srcCacheIndirection.template getPtr(); - dim3 const grid(roundUp(bh.nMaxSeqLen, 32), bh.nBatchSize * bh.nBeamWidth); + auto srcCI = ip->srcCacheIndirection->template getPtr(); + dim3 const grid(roundUp(bh.nMaxSeqLen, 32), bh.nBatchSize, bh.nBeamWidth); updateCacheIndirectionKernel<<>>( tgtCI, srcCI, bh, ip->maxAttentionWindow, ip->sinkTokenLength); sync_check_cuda_error(); @@ -164,79 +185,6 @@ void BeamSearchLayer::forwardAsyncSingleRequest( TLLM_LOG_TRACE("%s stop", __PRETTY_FUNCTION__); } -template -void BeamSearchLayer::forwardAsync( - std::shared_ptr const& baseOutputs, std::shared_ptr const& baseInputs) -{ - TLLM_LOG_TRACE("%s start", __PRETTY_FUNCTION__); - auto outputs = std::dynamic_pointer_cast(baseOutputs); - auto params = std::dynamic_pointer_cast(baseInputs); - - auto const localDecoderDomain = getLocalDecoderDomain(params, mDecoderDomain); - - auto batchSlots = params->batchSlots ? params->batchSlots->template getPtr() : nullptr; - auto const maxSeqLen = outputs->outputIds.shape[outputs->outputIds.shape.size() - 1]; - auto const ite = params->ite; - auto const step = params->step; - - // common inputs - auto const& endIds = params->endIds; - auto const localBatchSize = static_cast(params->localBatchSize); - - TLLM_CHECK_WITH_INFO(localDecoderDomain.getBeamWidth() > 1, - "Decoding mode is beam search, but beamWidth <= 1 (%d <= 1)", localDecoderDomain.getBeamWidth()); - TLLM_CHECK_WITH_INFO(params->srcCacheIndirection.has_value(), "srcCacheIndirection is mandatory in beam search."); - TLLM_CHECK_WITH_INFO(outputs->parentIds.has_value(), "parentIds tensor is mandatory in beam search."); - TLLM_CHECK_WITH_INFO(outputs->finished.has_value(), "finished tensor is mandatory in beam search."); - TLLM_CHECK_WITH_INFO(outputs->cumLogProbs.has_value(), "cumLogProbs tensor is mandatory in beam search."); - - // Compute one by one if there are different runtime arguments - // due to Batch-Beam-Search is not supported yet, so we need to compute - size_t const dynamic_decode_batch_size = mHasDiffRuntimeArgs ? 1 : localBatchSize; - auto const dynamic_decode_total_iteration = mHasDiffRuntimeArgs ? localBatchSize : 1; - - for (uint32_t dynamic_ite = 0; dynamic_ite < dynamic_decode_total_iteration; ++dynamic_ite) - { - auto const dynamic_id_offset = dynamic_ite * dynamic_decode_batch_size * localDecoderDomain.getBeamWidth(); - auto const dynamic_decode_vocab_size_units_offset = dynamic_id_offset * mDecoderDomain.getVocabSizePadded(); - - auto const logits_offset - = params->logits->slice({dynamic_decode_batch_size, params->logits->shape[1], params->logits->shape[2]}, - dynamic_decode_vocab_size_units_offset); - auto const end_id_offset = endIds.slice({dynamic_decode_batch_size}, dynamic_ite * dynamic_decode_batch_size); - - auto forwardParams = std::make_shared(step, ite, logits_offset, end_id_offset, - *params->srcCacheIndirection, static_cast(params->maxAttentionWindow), - static_cast(params->sinkTokenLength), static_cast(maxSeqLen), - dynamic_decode_batch_size); - - if (params->inputLengths) - { - forwardParams->inputLengths = params->inputLengths->slice( - {dynamic_decode_batch_size * localDecoderDomain.getBeamWidth()}, dynamic_id_offset); - } - - auto outputParams = std::make_shared(outputs->outputIds); - - outputParams->parentIds = std::move(outputs->parentIds); - outputParams->tgtCacheIndirection = std::move(outputs->tgtCacheIndirection); - outputParams->outputIdsPtr = std::move(outputs->outputIdsPtr); - outputParams->parentIdsPtr = std::move(outputs->parentIdsPtr); - outputParams->sequenceLength = outputs->sequenceLength->slice( - {dynamic_decode_batch_size * localDecoderDomain.getBeamWidth()}, dynamic_id_offset); - outputParams->finished = outputs->finished->slice( - {dynamic_decode_batch_size * localDecoderDomain.getBeamWidth()}, dynamic_id_offset); - outputParams->cumLogProbs = outputs->cumLogProbs->slice( - {dynamic_decode_batch_size * localDecoderDomain.getBeamWidth()}, dynamic_id_offset); - outputParams->outputLogProbs = outputs->outputLogProbsTiled; // notice: use tiled tensor - outputParams->beamHypotheses = std::move(outputs->beamHypotheses); - - // beamSearchDiversityRate is only supported when using BeamHypotheses - forwardAsyncSingleRequest(outputParams, forwardParams); - } // end of dynamic_ite - TLLM_LOG_TRACE("%s stop", __PRETTY_FUNCTION__); -} - template void BeamSearchLayer::allocateBuffer(runtime::SizeType32 const batchSize, runtime::SizeType32 const beamWidth) { @@ -247,9 +195,12 @@ void BeamSearchLayer::allocateBuffer(runtime::SizeType32 const batchSize, run size_t nTempBuffer = batchSize * nPadBeamWidth * nMaxVocabPartForStage1FastKernel * (2 * (nPadBeamWidth * 2) + 2); mWorkspaceSize = roundUp(nTopK, 4) * 2 + roundUp(nTempBuffer, 4); mWorkspace = mAllocator->reMalloc(mWorkspace, sizeof(float) * mWorkspaceSize, true); - mDiversityRateDevice = mAllocator->reMalloc(mDiversityRateDevice, sizeof(float) * batchSize, false); - mLengthPenaltyDevice = mAllocator->reMalloc(mLengthPenaltyDevice, sizeof(float) * batchSize, false); - mEarlyStoppingDevice = mAllocator->reMalloc(mEarlyStoppingDevice, sizeof(int) * batchSize, false); + mDiversityRateDevice + = mAllocator->reMalloc(mDiversityRateDevice, sizeof(float) * mDecoderDomain.getBatchSize(), false); + mLengthPenaltyDevice + = mAllocator->reMalloc(mLengthPenaltyDevice, sizeof(float) * mDecoderDomain.getBatchSize(), false); + mEarlyStoppingDevice + = mAllocator->reMalloc(mEarlyStoppingDevice, sizeof(int) * mDecoderDomain.getBatchSize(), false); mIsAllocateBuffer = true; TLLM_LOG_TRACE("%s stop", __PRETTY_FUNCTION__); } diff --git a/cpp/tensorrt_llm/layers/beamSearchLayer.h b/cpp/tensorrt_llm/layers/beamSearchLayer.h index fbecb1305..548dde560 100644 --- a/cpp/tensorrt_llm/layers/beamSearchLayer.h +++ b/cpp/tensorrt_llm/layers/beamSearchLayer.h @@ -20,6 +20,7 @@ #include "tensorrt_llm/layers/baseLayer.h" #include "tensorrt_llm/layers/decodingParams.h" #include "tensorrt_llm/runtime/common.h" +#include "tensorrt_llm/runtime/decodingOutput.h" #include #include @@ -29,30 +30,6 @@ namespace tc = tensorrt_llm::common; namespace tensorrt_llm::layers { -class BeamSearchInputParams : public DecodingInputs -{ -public: - explicit BeamSearchInputParams(runtime::SizeType32 step, runtime::SizeType32 ite, tc::Tensor logits, - tc::Tensor endIds, tc::Tensor srcCacheIndirection, runtime::SizeType32 maxAttentionWindow, - runtime::SizeType32 sinkTokenLength, runtime::SizeType32 maxSeqLen, runtime::SizeType32 localBatchSize) - : DecodingInputs(std::move(endIds), step, ite, localBatchSize) - , logits{std::move(logits)} - , maxAttentionWindow{maxAttentionWindow} - , sinkTokenLength{sinkTokenLength} - , maxSeqLen{maxSeqLen} - , srcCacheIndirection{std::move(srcCacheIndirection)} - { - } - - // mandatory parameters - tc::Tensor logits; // [maxBatchSize, beamWidth, vocabSizePadded] - runtime::SizeType32 maxAttentionWindow; - runtime::SizeType32 sinkTokenLength; - runtime::SizeType32 maxSeqLen; - tc::Tensor srcCacheIndirection; // [BS, BM, mSL] - std::optional inputLengths; // [BS, BM] -}; - template class BeamSearchLayer : public BaseLayer { @@ -70,13 +47,9 @@ class BeamSearchLayer : public BaseLayer std::shared_ptr const& inputs) override; private: - void forwardAsyncSingleRequest( - std::shared_ptr const& outputs, std::shared_ptr const& inputs); - void allocateBuffer(runtime::SizeType32 batchSize, runtime::SizeType32 beamWidth); void freeBuffer(); -private: using Base::mAllocator; using Base::mStream; @@ -94,7 +67,6 @@ class BeamSearchLayer : public BaseLayer std::vector mDiversityRateHost; std::vector mLengthPenaltyHost; std::vector mEarlyStoppingHost; - bool mHasDiffRuntimeArgs{false}; }; } // namespace tensorrt_llm::layers diff --git a/cpp/tensorrt_llm/layers/decodingLayer.cpp b/cpp/tensorrt_llm/layers/decodingLayer.cpp index 402dc887a..15b14bf20 100644 --- a/cpp/tensorrt_llm/layers/decodingLayer.cpp +++ b/cpp/tensorrt_llm/layers/decodingLayer.cpp @@ -123,7 +123,7 @@ void DecodingLayer::setup(SizeType32 batchSize, SizeType32 beamWidth, SizeTyp else if (mDecodingMode.isBeamSearch()) { // beam search layer TLLM_CHECK_WITH_INFO(beamWidth > 1, "Decoding mode is beam search, but beamWidth <= 1 (%d <= 1)", beamWidth); - mDecodingLayer->setup(batchSize, beamWidth, nullptr, setupParams->decodingParams); + mDecodingLayer->setup(batchSize, beamWidth, batchSlots, setupParams->decodingParams); } else if (mDecodingMode.isMedusa()) { diff --git a/cpp/tensorrt_llm/layers/dynamicDecodeLayer.cpp b/cpp/tensorrt_llm/layers/dynamicDecodeLayer.cpp index 0dd41361d..59f7dd5db 100644 --- a/cpp/tensorrt_llm/layers/dynamicDecodeLayer.cpp +++ b/cpp/tensorrt_llm/layers/dynamicDecodeLayer.cpp @@ -237,7 +237,7 @@ void DynamicDecodeLayer::prepareIdsPtrs(std::shared_ptr if (beamWidth > 1) { idsPtrHost[mDecoderDomain.getBatchSize() + batchSlot] - = outputs->parentIds.value().template getPtrWithOffset(bi * beamWidth * maxSeqLen); + = outputs->parentIds.value().template getPtrWithOffset(batchSlot * beamWidth * maxSeqLen); } else { diff --git a/cpp/tensorrt_llm/plugins/bertAttentionPlugin/bertAttentionPlugin.cpp b/cpp/tensorrt_llm/plugins/bertAttentionPlugin/bertAttentionPlugin.cpp index 290cd879b..f2c274d6d 100644 --- a/cpp/tensorrt_llm/plugins/bertAttentionPlugin/bertAttentionPlugin.cpp +++ b/cpp/tensorrt_llm/plugins/bertAttentionPlugin/bertAttentionPlugin.cpp @@ -32,13 +32,12 @@ static char const* BERT_ATTENTION_PLUGIN_NAME{"BertAttention"}; PluginFieldCollection BertAttentionPluginCreator::mFC{}; std::vector BertAttentionPluginCreator::mPluginAttributes; -BertAttentionPlugin::BertAttentionPlugin(int num_heads, int head_size, float q_scaling, bool qk_half_accum, +BertAttentionPlugin::BertAttentionPlugin(int num_heads, int head_size, float q_scaling, ContextFMHAType context_fmha_type, nvinfer1::DataType type, bool do_relative_attention, int max_distance, bool remove_padding) : mNumHeads(num_heads) , mHeadSize(head_size) , mQScaling(q_scaling) - , mQKHalfAccum(qk_half_accum) , mEnableContextFMHA(context_fmha_type != ContextFMHAType::DISABLED) , mFMHAForceFP32Acc(context_fmha_type == ContextFMHAType::ENABLED_WITH_FP32_ACC) , mType(type) @@ -539,7 +538,6 @@ BertAttentionPluginCreator::BertAttentionPluginCreator() mPluginAttributes.emplace_back(PluginField("num_heads", nullptr, PluginFieldType::kINT32, -1)); mPluginAttributes.emplace_back(PluginField("head_size", nullptr, PluginFieldType::kINT32, -1)); mPluginAttributes.emplace_back(PluginField("q_scaling", nullptr, PluginFieldType::kFLOAT32, 1.0)); - mPluginAttributes.emplace_back(PluginField("enable_qk_half_accum", nullptr, PluginFieldType::kINT8, 0)); mPluginAttributes.emplace_back(PluginField("context_fmha_type", nullptr, PluginFieldType::kINT8, 0)); mPluginAttributes.emplace_back(PluginField("type_id", nullptr, PluginFieldType::kINT32, 1)); mPluginAttributes.emplace_back(PluginField("do_relative_attention", nullptr, PluginFieldType::kINT8, 0)); @@ -569,7 +567,6 @@ IPluginV2* BertAttentionPluginCreator::createPlugin(char const* name, PluginFiel PluginField const* fields = fc->fields; int num_heads, head_size; ContextFMHAType context_fmha_type; - bool qk_half_accum; float q_scaling; nvinfer1::DataType type; bool do_relative_attention; @@ -594,11 +591,6 @@ IPluginV2* BertAttentionPluginCreator::createPlugin(char const* name, PluginFiel TLLM_CHECK(fields[i].type == PluginFieldType::kFLOAT32); q_scaling = static_cast(*(static_cast(fields[i].data))); } - else if (!strcmp(attrName, "enable_qk_half_accum")) - { - TLLM_CHECK(fields[i].type == PluginFieldType::kINT8); - qk_half_accum = static_cast(*(static_cast(fields[i].data))); - } else if (!strcmp(attrName, "context_fmha_type")) { TLLM_CHECK(fields[i].type == PluginFieldType::kINT8); @@ -627,7 +619,7 @@ IPluginV2* BertAttentionPluginCreator::createPlugin(char const* name, PluginFiel } try { - auto* obj = new BertAttentionPlugin(num_heads, head_size, q_scaling, qk_half_accum, context_fmha_type, type, + auto* obj = new BertAttentionPlugin(num_heads, head_size, q_scaling, context_fmha_type, type, do_relative_attention, max_distance, remove_padding); obj->setPluginNamespace(mNamespace.c_str()); return obj; diff --git a/cpp/tensorrt_llm/plugins/bertAttentionPlugin/bertAttentionPlugin.h b/cpp/tensorrt_llm/plugins/bertAttentionPlugin/bertAttentionPlugin.h index eb7d42c98..b8e307be6 100644 --- a/cpp/tensorrt_llm/plugins/bertAttentionPlugin/bertAttentionPlugin.h +++ b/cpp/tensorrt_llm/plugins/bertAttentionPlugin/bertAttentionPlugin.h @@ -34,7 +34,7 @@ class BertAttentionPlugin : public BasePlugin public: BertAttentionPlugin() = delete; - BertAttentionPlugin(int num_heads, int head_size, float q_scaling, bool qk_half_accum, + BertAttentionPlugin(int num_heads, int head_size, float q_scaling, tensorrt_llm::kernels::ContextFMHAType context_fmha_type, nvinfer1::DataType type, bool do_relative_attention = false, int max_distance = 0, bool remove_padding = false); diff --git a/cpp/tensorrt_llm/plugins/gemmPlugin/gemmPlugin.cpp b/cpp/tensorrt_llm/plugins/gemmPlugin/gemmPlugin.cpp index d5af27563..e56e10ed7 100644 --- a/cpp/tensorrt_llm/plugins/gemmPlugin/gemmPlugin.cpp +++ b/cpp/tensorrt_llm/plugins/gemmPlugin/gemmPlugin.cpp @@ -359,6 +359,16 @@ int GemmPlugin::enqueue(nvinfer1::PluginTensorDesc const* inputDesc, nvinfer1::P int const K = static_cast( mTransA ? inputDesc[0].dims.d[0] - padK : inputDesc[0].dims.d[nbDimsA - 1] - padK); + bool noPadDim = padM == 0 && padN == 0 && padK == 0; + bool cudaKernelSupportType = mType == nvinfer1::DataType::kHALF || mType == nvinfer1::DataType::kFLOAT + || mType == nvinfer1::DataType::kBF16; + + // skip computation for a TRT empty tensor + if (M == 0) + { + return 0; + } + std::string mnkStr = "MNK={" + std::to_string(M) + ", " + std::to_string(N) + ", " + std::to_string(K) + "}"; { std::string const activationStr = "GEMM layer's activation before GEMM with " + mnkStr; @@ -367,31 +377,26 @@ int GemmPlugin::enqueue(nvinfer1::PluginTensorDesc const* inputDesc, nvinfer1::P "Found NaN in " + activationStr); } + bool cudaKernelFinished = false; // TODO: sub tensor matmul is not supported in fp8 gemm cuda kernel - if (M <= 4 && mUseFp8 && padM == 0 && padN == 0 && padK == 0) + if (M <= 4 && N <= 128000 && mUseFp8 && noPadDim && cudaKernelSupportType) { tensorrt_llm::common::QuantMode quantMode = tensorrt_llm::common::QuantMode::fromQuantAlgo("FP8"); tensorrt_llm::kernels::fp8_gemm::Params params(reinterpret_cast(inputs[0]), - reinterpret_cast(inputs[1]), mAlpha, reinterpret_cast(outputs[0]), M, N, K, quantMode); - if (mType == nvinfer1::DataType::kHALF) - { - tensorrt_llm::kernels::fp8_gemm::fp8GemmLauncher<__nv_fp8_e4m3, half>(params, stream); - } - else if (mType == nvinfer1::DataType::kFLOAT) - { - tensorrt_llm::kernels::fp8_gemm::fp8GemmLauncher<__nv_fp8_e4m3, float>(params, stream); - } - else if (mType == nvinfer1::DataType::kBF16) - { - tensorrt_llm::kernels::fp8_gemm::fp8GemmLauncher<__nv_fp8_e4m3, __nv_bfloat16>(params, stream); - } - else - { - TLLM_LOG_ERROR( - "tensorrt_llm::kernels::fp8_gemm::fp8GemmLauncher could only support dtype={half, bf16, float}"); - } + reinterpret_cast(inputs[1]), mAlpha, reinterpret_cast(outputs[0]), M, N, K, quantMode, + nvinfer1::DataType::kFP8, mOutputType); + cudaKernelFinished = tensorrt_llm::kernels::fp8_gemm::fp8GemmDispatcher(params, stream); } - else + else if (M <= 6 && N <= 128000 && !mUseFp8 && noPadDim && cudaKernelSupportType) + { + tensorrt_llm::common::QuantMode quantMode; + tensorrt_llm::kernels::fp8_gemm::Params params(reinterpret_cast(inputs[0]), + reinterpret_cast(inputs[1]), mAlpha, reinterpret_cast(outputs[0]), M, N, K, quantMode, + mType, mOutputType); + cudaKernelFinished = tensorrt_llm::kernels::fp8_gemm::fp8GemmDispatcher(params, stream); + } + + if (!cudaKernelFinished) { auto bestTactic = mPluginProfiler->getBestConfig(M, mGemmId); runGemm(M, N, K, mTransA, mTransB, mPadLda, mPadLdb, mType, mCublasWrapper, inputs[0], inputs[1], mAlpha, diff --git a/cpp/tensorrt_llm/plugins/gptAttentionCommon/gptAttentionCommon.cpp b/cpp/tensorrt_llm/plugins/gptAttentionCommon/gptAttentionCommon.cpp index 045eced23..5e35935e1 100644 --- a/cpp/tensorrt_llm/plugins/gptAttentionCommon/gptAttentionCommon.cpp +++ b/cpp/tensorrt_llm/plugins/gptAttentionCommon/gptAttentionCommon.cpp @@ -56,7 +56,7 @@ struct FusedQKVMaskedAttentionDispatchParams T const* qkv_bias; T const* relative_attention_bias; int const* cache_indir; - T* context_buf; + void* context_buf; bool const* finished; int const* sequence_lengths; int max_batch_size; @@ -273,7 +273,7 @@ void fusedQKV_masked_attention_dispatch(Multihead_attention_params(input_params.context_buf); + params.out = input_params.context_buf; // Set the input buffers. params.q = reinterpret_cast(input_params.qkv_buf); @@ -1250,12 +1250,12 @@ int GPTAttentionPluginCommon::enqueueContext(EnqueueContextParams(params.context_buf), qkv_buf_2_, params.batch_size, attention_seq_len_1, + mNumHeads, getHeadSize(), (float*) nullptr, 0, stream); } else { - invokeTransposeAttentionOutRemovePadding(qkv_buf_2_, params.context_buf, params.num_tokens, + invokeTransposeAttentionOutRemovePadding(qkv_buf_2_, static_cast(params.context_buf), params.num_tokens, params.batch_size, attention_seq_len_1, mNumHeads, getHeadSize(), padding_offset, (float*) nullptr, 0, stream); } @@ -1350,7 +1350,7 @@ int GPTAttentionPluginCommon::enqueueGeneration( if (tensorrt_llm::kernels::XQADispatchHelper::CanSupport && mDecoderXQARunner.get() != nullptr && this->template convertMMHAParamsToXQAParams( xqaParams, params, /*forConfigurePlugin=*/false) - && mDecoderXQARunner->template shouldUse(xqaParams, /*forConfigurePlugin=*/false)) + && mDecoderXQARunner->shouldUse(xqaParams, /*forConfigurePlugin=*/false)) { TLLM_LOG_DEBUG("XQA kernels are selected in the generation phase."); mDecoderXQARunner->template dispatch(xqaParams, kv_cache_buffer, stream); @@ -1529,7 +1529,7 @@ void GPTAttentionPluginCommon::prepareEnqueueGeneration(EnqueueGenerationParams< XQAParams xqaParams{}; if (tensorrt_llm::kernels::XQADispatchHelper::CanSupport && mDecoderXQARunner.get() != nullptr && this->template convertMMHAParamsToXQAParams(xqaParams, params, /*forConfigurePlugin=*/true) - && mDecoderXQARunner->template shouldUse(xqaParams, /*forConfigurePlugin=*/true)) + && mDecoderXQARunner->shouldUse(xqaParams, /*forConfigurePlugin=*/true)) { mDecoderXQARunner->prepare(xqaParams); } diff --git a/cpp/tensorrt_llm/plugins/gptAttentionCommon/gptAttentionCommon.h b/cpp/tensorrt_llm/plugins/gptAttentionCommon/gptAttentionCommon.h index cfe9f870e..90f34fe2a 100644 --- a/cpp/tensorrt_llm/plugins/gptAttentionCommon/gptAttentionCommon.h +++ b/cpp/tensorrt_llm/plugins/gptAttentionCommon/gptAttentionCommon.h @@ -111,7 +111,7 @@ class GPTAttentionPluginCommon : public BasePlugin float const* kv_scale_quant_orig; float const* attention_output_orig_quant; T const* alibi_slopes; - T* context_buf; + void* context_buf; void* key_value_cache; kernels::KVBlockArray::DataType* block_offsets; kernels::KVBlockArray::DataType* host_block_offsets; @@ -195,7 +195,7 @@ class GPTAttentionPluginCommon : public BasePlugin float const* attention_output_orig_quant; float const* rotary_embedding_scaling_factors; T const* alibi_slopes; - T* context_buf; + void* context_buf; void* key_value_cache; kernels::KVBlockArray::DataType* block_offsets; void* host_primary_pool_pointer; diff --git a/cpp/tensorrt_llm/plugins/gptAttentionPlugin/gptAttentionPlugin.cpp b/cpp/tensorrt_llm/plugins/gptAttentionPlugin/gptAttentionPlugin.cpp index a96a35834..a716b039e 100644 --- a/cpp/tensorrt_llm/plugins/gptAttentionPlugin/gptAttentionPlugin.cpp +++ b/cpp/tensorrt_llm/plugins/gptAttentionPlugin/gptAttentionPlugin.cpp @@ -397,7 +397,7 @@ static size_t getStride(nvinfer1::Dims const& dims, int n) return std::accumulate(dims.d + n + 1, dims.d + dims.nbDims, 1, std::multiplies{}); } -template +template int GPTAttentionPlugin::enqueueImpl(nvinfer1::PluginTensorDesc const* inputDesc, nvinfer1::PluginTensorDesc const* outputDesc, void const* const* inputs, void* const* outputs, void* workspace, cudaStream_t stream) @@ -437,8 +437,8 @@ int GPTAttentionPlugin::enqueueImpl(nvinfer1::PluginTensorDesc const* inputDesc, auto seqIdxBeg = 0; auto tokenIdxBeg = 0; auto localNbTokens = contextTokenIdxEnd; - enqueueSome(seqIdxBeg, nbContextRequests, tokenIdxBeg, localNbTokens, inputDesc, outputDesc, - inputs, outputs, workspace, stream); + enqueueSome(seqIdxBeg, nbContextRequests, tokenIdxBeg, localNbTokens, + inputDesc, outputDesc, inputs, outputs, workspace, stream); } if (auto nbGenerationSeq = nbSeq - nbContextRequests; nbGenerationSeq > 0) @@ -451,8 +451,8 @@ int GPTAttentionPlugin::enqueueImpl(nvinfer1::PluginTensorDesc const* inputDesc, auto localNbTokens = mRemovePadding ? inputDesc[getIdx(IdxEntry::QKV_TENSOR)].dims.d[0] - contextTokenIdxEnd : inputDesc[getIdx(IdxEntry::QKV_TENSOR)].dims.d[0] * inputDesc[getIdx(IdxEntry::QKV_TENSOR)].dims.d[1]; - enqueueSome(seqIdxBeg, nbGenerationSeq, tokenIdxBeg, localNbTokens, inputDesc, outputDesc, - inputs, outputs, workspace, stream); + enqueueSome(seqIdxBeg, nbGenerationSeq, tokenIdxBeg, localNbTokens, inputDesc, + outputDesc, inputs, outputs, workspace, stream); } TLLM_LOG_TRACE("Attention plugin stop at layer %d", mLayerIdx); @@ -460,7 +460,7 @@ int GPTAttentionPlugin::enqueueImpl(nvinfer1::PluginTensorDesc const* inputDesc, return 0; } -template +template int GPTAttentionPlugin::enqueueSome(int32_t seqIdxBeg, int32_t localNbSeq, int32_t tokenIdxBeg, int32_t localNbTokens, nvinfer1::PluginTensorDesc const* inputDesc, nvinfer1::PluginTensorDesc const* outputDesc, void const* const* inputs, void* const* outputs, void* workspace, cudaStream_t stream) @@ -621,10 +621,8 @@ int GPTAttentionPlugin::enqueueSome(int32_t seqIdxBeg, int32_t localNbSeq, int32 host_secondary_pool_pointer = reinterpret_cast(typed_host_pool_pointers[1] + layerOffset); } - // FP8 output when fp8_context_fmha is enabled. - auto const outputElemSize = (mFP8ContextFMHA ? 1 : sizeof(T)); - T* context_buf_ = reinterpret_cast(static_cast(outputs[0]) - + outputDesc[0].dims.d[getPackedTensorHiddenDimIndex(mRemovePadding)] * tokenIdxBeg * outputElemSize); + AttentionOutT* context_buf_ = static_cast(outputs[0]) + + outputDesc[0].dims.d[getPackedTensorHiddenDimIndex(mRemovePadding)] * tokenIdxBeg; void* key_value_cache = nullptr; if (useKVCache() && !mPagedKVCache) { @@ -794,18 +792,18 @@ int GPTAttentionPlugin::enqueueSome(int32_t seqIdxBeg, int32_t localNbSeq, int32 return 0; } -template +template int GPTAttentionPlugin::enqueueDispatchKVCacheType(nvinfer1::PluginTensorDesc const* inputDesc, nvinfer1::PluginTensorDesc const* outputDesc, void const* const* inputs, void* const* outputs, void* workspace, cudaStream_t stream) { if (mPagedKVCache) { - return enqueueImpl(inputDesc, outputDesc, inputs, outputs, workspace, stream); + return enqueueImpl(inputDesc, outputDesc, inputs, outputs, workspace, stream); } else { - return enqueueImpl(inputDesc, outputDesc, inputs, outputs, workspace, stream); + return enqueueImpl(inputDesc, outputDesc, inputs, outputs, workspace, stream); } return 0; } @@ -820,6 +818,13 @@ int GPTAttentionPlugin::enqueue(nvinfer1::PluginTensorDesc const* inputDesc, } if (mType == nvinfer1::DataType::kHALF) { +#ifdef ENABLE_FP8 + if (mFP8ContextFMHA) + { + return enqueueDispatchKVCacheType( + inputDesc, outputDesc, inputs, outputs, workspace, stream); + } +#endif return enqueueDispatchKVCacheType(inputDesc, outputDesc, inputs, outputs, workspace, stream); } else if (mType == nvinfer1::DataType::kFLOAT) @@ -829,6 +834,13 @@ int GPTAttentionPlugin::enqueue(nvinfer1::PluginTensorDesc const* inputDesc, #ifdef ENABLE_BF16 else if (mType == nvinfer1::DataType::kBF16) { +#ifdef ENABLE_FP8 + if (mFP8ContextFMHA) + { + return enqueueDispatchKVCacheType<__nv_bfloat16, __nv_fp8_e4m3>( + inputDesc, outputDesc, inputs, outputs, workspace, stream); + } +#endif return enqueueDispatchKVCacheType<__nv_bfloat16>(inputDesc, outputDesc, inputs, outputs, workspace, stream); } #endif diff --git a/cpp/tensorrt_llm/plugins/gptAttentionPlugin/gptAttentionPlugin.h b/cpp/tensorrt_llm/plugins/gptAttentionPlugin/gptAttentionPlugin.h index 4c27d4981..e41e42c91 100644 --- a/cpp/tensorrt_llm/plugins/gptAttentionPlugin/gptAttentionPlugin.h +++ b/cpp/tensorrt_llm/plugins/gptAttentionPlugin/gptAttentionPlugin.h @@ -111,11 +111,11 @@ class GPTAttentionPlugin : public GPTAttentionPluginCommon int enqueue(nvinfer1::PluginTensorDesc const* inputDesc, nvinfer1::PluginTensorDesc const* outputDesc, void const* const* inputs, void* const* outputs, void* workspace, cudaStream_t stream) noexcept override; - template + template int enqueueImpl(nvinfer1::PluginTensorDesc const* inputDesc, nvinfer1::PluginTensorDesc const* outputDesc, void const* const* inputs, void* const* outputs, void* workspace, cudaStream_t stream); - template + template int enqueueDispatchKVCacheType(nvinfer1::PluginTensorDesc const* inputDesc, nvinfer1::PluginTensorDesc const* outputDesc, void const* const* inputs, void* const* outputs, void* workspace, cudaStream_t stream); @@ -152,7 +152,7 @@ class GPTAttentionPlugin : public GPTAttentionPluginCommon }; private: - template + template int enqueueSome(int32_t seqIdxBeg, int32_t localNbSeq, int32_t tokenIdxBeg, int32_t localNbTokens, nvinfer1::PluginTensorDesc const* inputDesc, nvinfer1::PluginTensorDesc const* outputDesc, void const* const* inputs, void* const* outputs, void* workspace, cudaStream_t stream); diff --git a/cpp/tensorrt_llm/plugins/loraPlugin/loraPlugin.cpp b/cpp/tensorrt_llm/plugins/loraPlugin/loraPlugin.cpp index 0bbc3ccd3..2cf748762 100644 --- a/cpp/tensorrt_llm/plugins/loraPlugin/loraPlugin.cpp +++ b/cpp/tensorrt_llm/plugins/loraPlugin/loraPlugin.cpp @@ -321,6 +321,23 @@ void runCublasGemmEx(int const M, int const N, int const K, bool const transA, b TLLM_LOG_TRACE("%s stop", __PRETTY_FUNCTION__); } +bool isEnableLora(int batchSize, int numLoraModules, void const* const* loraRanks) +{ + for (int batchIdx = 0; batchIdx < batchSize; batchIdx++) + { + for (int loraModuleIdx = 0; loraModuleIdx < numLoraModules; loraModuleIdx++) + { + auto const loraRank = static_cast(loraRanks[loraModuleIdx]); + if (loraRank[batchIdx] != 0) + { + return true; + } + } + } + + return false; +} + int LoraPlugin::enqueue(nvinfer1::PluginTensorDesc const* inputDesc, nvinfer1::PluginTensorDesc const* outputDesc, void const* const* inputs, void* const* outputs, void* workspace, cudaStream_t stream) noexcept { @@ -352,19 +369,26 @@ int LoraPlugin::enqueue(nvinfer1::PluginTensorDesc const* inputDesc, nvinfer1::P void* groupGemmParamsWorkSpace = static_cast(lowRankWorkSpace) + getLowRankWorkSpaceSize(batch_size, mMaxContextLength, mNumLoraModules, mMaxLowRank, typeSize); + bool isWithLora = isEnableLora(batch_size, mNumLoraModules, &inputs[getLoraRanksIdx()]); + int const nbDimsA = inputDesc[0].dims.nbDims; - for (int loraModuleIdx = 0; loraModuleIdx < mNumLoraModules; loraModuleIdx++) + if (!isWithLora) { - size_t size = 1; - for (int i = 0; i < outputDesc[loraModuleIdx].dims.nbDims; i++) + for (int loraModuleIdx = 0; loraModuleIdx < mNumLoraModules; loraModuleIdx++) { - size *= outputDesc[loraModuleIdx].dims.d[i]; + size_t size = 1; + for (int i = 0; i < outputDesc[loraModuleIdx].dims.nbDims; i++) + { + size *= outputDesc[loraModuleIdx].dims.d[i]; + } + cudaMemsetAsync(outputs[loraModuleIdx], 0, size * typeSize, stream); } - cudaMemsetAsync(outputs[loraModuleIdx], 0, size * typeSize, stream); + TLLM_LOG_TRACE("%s stop", __PRETTY_FUNCTION__); + return 0; } char* useUnifiedGemmChar = std::getenv("LORA_USE_UNIFIED_GEMM"); - bool useUnifiedGemm = (useUnifiedGemmChar != nullptr && std::string(useUnifiedGemmChar) == "ON"); + bool useUnifiedGemm = (useUnifiedGemmChar == nullptr || std::string(useUnifiedGemmChar) != "OFF"); for (int batchIdx = 0; batchIdx < batch_size; batchIdx++) { for (int loraModuleIdx = 0; loraModuleIdx < mNumLoraModules; loraModuleIdx++) @@ -372,7 +396,7 @@ int LoraPlugin::enqueue(nvinfer1::PluginTensorDesc const* inputDesc, nvinfer1::P auto const lora_ranks = static_cast(inputs[getLoraRanksIdx() + loraModuleIdx]); auto const lora_weights_ptr = static_cast(inputs[getLoraWeightsPtrsIdx() + loraModuleIdx]); if (lora_weights_ptr[batchIdx * 2] != lora_weights_ptr[0] - || lora_weights_ptr[batchIdx * 2 + 1] != lora_weights_ptr[1] || lora_ranks[batchIdx] == 0) + || lora_weights_ptr[batchIdx * 2 + 1] != lora_weights_ptr[1] || lora_ranks[batchIdx] != lora_ranks[0]) { useUnifiedGemm = false; } diff --git a/cpp/tensorrt_llm/plugins/ncclPlugin/allreducePlugin.cpp b/cpp/tensorrt_llm/plugins/ncclPlugin/allreducePlugin.cpp index eef20e8f5..ba7fb893d 100644 --- a/cpp/tensorrt_llm/plugins/ncclPlugin/allreducePlugin.cpp +++ b/cpp/tensorrt_llm/plugins/ncclPlugin/allreducePlugin.cpp @@ -171,6 +171,9 @@ AllReduceStrategyType AllreducePlugin::selectImplementation( if (messageSizeBytes <= maxWorkspaceSize) { + // In some instances, the two-shot strategy has exhibited significant performance issues. + // As a temporary measure, we have disabled the two-shot strategy. + // TODO: remove this WAR after https://nvbugspro.nvidia.com/bug/4718747 is fixed. if (!isAuto) { strat = mStrategy; @@ -187,7 +190,7 @@ AllReduceStrategyType AllreducePlugin::selectImplementation( } else { - strat = AllReduceStrategyType::TWOSHOT; + strat = AllReduceStrategyType::NCCL; } } else @@ -198,7 +201,7 @@ AllReduceStrategyType AllreducePlugin::selectImplementation( } else { - strat = AllReduceStrategyType::TWOSHOT; + strat = AllReduceStrategyType::NCCL; } } diff --git a/cpp/tensorrt_llm/pybind/executor/bindings.cpp b/cpp/tensorrt_llm/pybind/executor/bindings.cpp index 379eb7642..627a8678b 100644 --- a/cpp/tensorrt_llm/pybind/executor/bindings.cpp +++ b/cpp/tensorrt_llm/pybind/executor/bindings.cpp @@ -118,6 +118,7 @@ void InitBindings(pybind11::module_& m) .def_readwrite("iter", &tle::IterationStats::iter) .def_readwrite("iter_latency_ms", &tle::IterationStats::iterLatencyMS) .def_readwrite("num_active_requests", &tle::IterationStats::numActiveRequests) + .def_readwrite("num_queued_requests", &tle::IterationStats::numQueuedRequests) .def_readwrite("max_num_active_requests", &tle::IterationStats::maxNumActiveRequests) .def_readwrite("gpu_mem_usage", &tle::IterationStats::gpuMemUsage) .def_readwrite("cpu_mem_usage", &tle::IterationStats::cpuMemUsage) @@ -227,14 +228,15 @@ void InitBindings(pybind11::module_& m) std::optional const&, std::optional const&, std::optional>, std::optional>, std::optional, std::optional, std::optional, - std::optional, std::optional, std::optional>(), + std::optional, std::optional, std::optional, bool>(), py::arg("input_token_ids"), py::arg("max_new_tokens"), py::arg("streaming") = false, py::arg_v("sampling_config", tle::SamplingConfig(), "SamplingConfig()"), py::arg_v("output_config", tle::OutputConfig(), "OutputConfig()"), py::arg("end_id") = py::none(), py::arg("pad_id") = py::none(), py::arg("bad_words") = py::none(), py::arg("stop_words") = py::none(), py::arg("embedding_bias") = py::none(), py::arg("external_draft_tokens_config") = py::none(), py::arg("prompt_tuning_config") = py::none(), py::arg("lora_config") = py::none(), - py::arg("logits_post_processor_name") = py::none(), py::arg("encoder_input_token_ids") = py::none()) + py::arg("logits_post_processor_name") = py::none(), py::arg("encoder_input_token_ids") = py::none(), + py::arg("return_all_generated_tokens") = false) .def_property_readonly("input_token_ids", &tle::Request::getInputTokenIds) .def_property_readonly("max_new_tokens", &tle::Request::getMaxNewTokens) .def_property("streaming", &tle::Request::getStreaming, &tle::Request::setStreaming) @@ -253,7 +255,9 @@ void InitBindings(pybind11::module_& m) .def_property("logits_post_processor_name", &tle::Request::getLogitsPostProcessorName, &tle::Request::setLogitsPostProcessorName) .def_property( - "encoder_input_token_ids", &tle::Request::getEncoderInputTokenIds, &tle::Request::setEncoderInputTokenIds); + "encoder_input_token_ids", &tle::Request::getEncoderInputTokenIds, &tle::Request::setEncoderInputTokenIds) + .def_property("return_all_generated_tokens", &tle::Request::getReturnAllGeneratedTokens, + &tle::Request::setReturnAllGeneratedTokens); request.attr("BATCHED_POST_PROCESSOR_NAME") = tle::Request::kBatchedPostProcessorName; py::class_(m, "Result") @@ -450,14 +454,14 @@ void InitBindings(pybind11::module_& m) return py::make_tuple(self.getMaxBeamWidth(), schedulerConfigState, kvCacheConfigState, self.getEnableChunkedContext(), self.getNormalizeLogProbs(), self.getIterStatsMaxIterations(), - self.getRequestStatsMaxIterations(), self.getBatchingType(), self.getMaxBatchSize(), parallelConfigState, - peftCacheConfigState, self.getLogitsPostProcessorMap(), self.getLogitsPostProcessorBatched(), - self.getDecodingConfig(), self.getGpuWeightsPercent()); + self.getRequestStatsMaxIterations(), self.getBatchingType(), self.getMaxBatchSize(), self.getMaxNumTokens(), + parallelConfigState, peftCacheConfigState, self.getLogitsPostProcessorMap(), + self.getLogitsPostProcessorBatched(), self.getDecodingConfig(), self.getGpuWeightsPercent()); }; auto executorConfigSetState = [&kvCacheConfigSetstate, &peftCacheConfigSetstate, &schedulerConfigSetstate, ¶llelConfigSetstate](py::tuple state) { - if (state.size() != 15) + if (state.size() != 16) { throw std::runtime_error("Invalid state!"); } @@ -465,39 +469,44 @@ void InitBindings(pybind11::module_& m) auto schedulerConfig = schedulerConfigSetstate(state[1].cast()); std::optional peftCacheConfig; - if (state[10].cast() != py::none()) + if (state[11].cast() != py::none()) { - peftCacheConfig = peftCacheConfigSetstate(state[10].cast()); + peftCacheConfig = peftCacheConfigSetstate(state[11].cast()); } std::optional parallelConfig; - if (state[9].cast() != py::none()) + if (state[10].cast() != py::none()) { - parallelConfig = parallelConfigSetstate(state[9].cast()); + parallelConfig = parallelConfigSetstate(state[10].cast()); } return tle::ExecutorConfig(state[0].cast(), schedulerConfig, kvCacheConfig, state[3].cast(), state[4].cast(), state[5].cast(), state[6].cast(), - state[7].cast(), state[8].cast>(), parallelConfig, - peftCacheConfig, state[11].cast>(), - state[12].cast>(), - state[13].cast>(), state[14].cast()); + state[7].cast(), state[8].cast>(), + state[9].cast>(), parallelConfig, peftCacheConfig, + state[12].cast>(), + state[13].cast>(), + state[14].cast>(), state[15].cast()); }; py::class_(m, "ExecutorConfig") .def(py::init, std::optional, - tle::PeftCacheConfig const&, std::optional, - std::optional, std::optional, float>(), + SizeType32, tle::BatchingType, std::optional, std::optional, + std::optional, tle::PeftCacheConfig const&, + std::optional, std::optional, + std::optional, float>(), py::arg("max_beam_width") = 1, py::arg_v("scheduler_config", tle::SchedulerConfig(), "SchedulerConfig()"), py::arg_v("kv_cache_config", tle::KvCacheConfig(), "KvCacheConfig()"), py::arg("enable_chunked_context") = false, py::arg("normalize_log_probs") = true, py::arg("iter_stats_max_iterations") = tle::kDefaultIterStatsMaxIterations, py::arg("request_stats_max_iterations") = tle::kDefaultRequestStatsMaxIterations, py::arg_v("batching_type", tle::BatchingType::kINFLIGHT, "BatchingType.INFLIGHT"), - py::arg("max_batch_size") = py::none(), py::arg("parallel_config") = py::none(), + py::arg("max_batch_size") = py::none(), py::arg("max_num_tokens") = py::none(), + py::arg("parallel_config") = py::none(), py::arg_v("peft_cache_config", tle::PeftCacheConfig(), "PeftCacheConfig()"), py::arg("logits_post_processor_map") = py::none(), py::arg("logits_post_processor_batched") = py::none(), py::arg("decoding_config") = py::none(), py::arg("gpu_weights_percent") = 1.0) .def_property("max_beam_width", &tle::ExecutorConfig::getMaxBeamWidth, &tle::ExecutorConfig::setMaxBeamWidth) + .def_property("max_batch_size", &tle::ExecutorConfig::getMaxBatchSize, &tle::ExecutorConfig::setMaxBatchSize) + .def_property("max_num_tokens", &tle::ExecutorConfig::getMaxNumTokens, &tle::ExecutorConfig::setMaxNumTokens) .def_property( "scheduler_config", &tle::ExecutorConfig::getSchedulerConfig, &tle::ExecutorConfig::setSchedulerConfig) .def_property("kv_cache_config", &tle::ExecutorConfig::getKvCacheConfig, &tle::ExecutorConfig::setKvCacheConfig) diff --git a/cpp/tensorrt_llm/runtime/gptDecoder.cpp b/cpp/tensorrt_llm/runtime/gptDecoder.cpp index e650a2c54..52cd031c5 100644 --- a/cpp/tensorrt_llm/runtime/gptDecoder.cpp +++ b/cpp/tensorrt_llm/runtime/gptDecoder.cpp @@ -57,9 +57,11 @@ GptDecoder::GptDecoder(executor::DecodingMode const& mode, size_t maxBatchSiz } template -void GptDecoder::setup(SamplingConfig const& samplingConfig, size_t batchSize, - std::optional const& batchSlots, std::optional const& output) +void GptDecoder::setup(SamplingConfig const& samplingConfig, size_t batchSize, SizeType32 const* batchSlots, + std::optional const& output) { + TLLM_LOG_TRACE("%s start", __PRETTY_FUNCTION__); + mSamplingConfig = samplingConfig; auto setupParams = std::make_shared(); @@ -135,8 +137,9 @@ void GptDecoder::setup(SamplingConfig const& samplingConfig, size_t batchSize setupParams->decodingParams->randomSeed = mSamplingConfig.randomSeed; - auto const batchSlotsPtr = batchSlots.has_value() ? bufferCast(*(batchSlots.value())) : nullptr; - mDynamicDecodeLayer->setup(batchSize, mSamplingConfig.beamWidth, batchSlotsPtr, setupParams); + mDynamicDecodeLayer->setup(batchSize, mSamplingConfig.beamWidth, batchSlots, setupParams); + + TLLM_LOG_TRACE("%s stop", __PRETTY_FUNCTION__); } namespace @@ -543,7 +546,8 @@ void GptDecoder::forwardSync(DecodingOutput& output, DecodingInput const& inp // Must be similar to [cpp/tensorrt_llm/thop/gatherTreeOp.cpp] gatherTree template void GptDecoder::gatherTree(ITensor& finalOutputIds, DecodingOutput const& decodingOutput, - DecodingInput const& decodingInput, BufferManager const& manager) + DecodingInput const& decodingInput, BufferManager const& manager, + std::optional> samplingConfig) { TLLM_LOG_TRACE("%s start", __PRETTY_FUNCTION__); auto const& finalOutputIdsShape = finalOutputIds.getShape(); @@ -570,25 +574,26 @@ void GptDecoder::gatherTree(ITensor& finalOutputIds, DecodingOutput const& de bufferCast(*decodingInput.endIds), batchSize * beamWidth, maxSeqLength, stream); sync_check_cuda_error(); - // Prepare length penalty, use the value from mSamplingConfig or 1.0f by default + // Prepare length penalty, use the value from samplingConfig or 1.0f by default + SamplingConfig const& samplingConf = samplingConfig ? (*samplingConfig).get() : mSamplingConfig; std::vector lengthPenaltyVec; TensorPtr lengthPenaltyPtr = std::shared_ptr(manager.gpu(ITensor::makeShape({batchSize}), TRTDataType::value)); - if (!mSamplingConfig.lengthPenalty.has_value() || mSamplingConfig.lengthPenalty.value().size() == 0) + if (!samplingConf.lengthPenalty.has_value() || samplingConf.lengthPenalty.value().size() == 0) { lengthPenaltyVec = std::vector(batchSize, 1.0f); } - else if (long int const size = mSamplingConfig.lengthPenalty.value().size(); size == 1) + else if (long int const size = samplingConf.lengthPenalty.value().size(); size == 1) { - lengthPenaltyVec = std::vector(batchSize, mSamplingConfig.lengthPenalty.value()[0]); + lengthPenaltyVec = std::vector(batchSize, samplingConf.lengthPenalty.value()[0]); } else { TLLM_CHECK_WITH_INFO(size == batchSize, - common::fmtstr("Size of lengthPenalty in SimplingConfig (" FMT_DIM ") is different from batchSize (" FMT_DIM + common::fmtstr("Size of lengthPenalty in SamplingConfig (" FMT_DIM ") is different from batchSize (" FMT_DIM ")", size, batchSize)); - lengthPenaltyVec = mSamplingConfig.lengthPenalty.value(); + lengthPenaltyVec = samplingConf.lengthPenalty.value(); } lengthPenaltyPtr = manager.copyFrom(lengthPenaltyVec, ITensor::makeShape({batchSize}), runtime::MemoryType::kGPU); diff --git a/cpp/tensorrt_llm/runtime/gptDecoderBatch.cpp b/cpp/tensorrt_llm/runtime/gptDecoderBatch.cpp index 1c5c22ba6..7c25ac5fb 100644 --- a/cpp/tensorrt_llm/runtime/gptDecoderBatch.cpp +++ b/cpp/tensorrt_llm/runtime/gptDecoderBatch.cpp @@ -19,7 +19,6 @@ #include "tensorrt_llm/kernels/decodingCommon.h" #include "tensorrt_llm/runtime/bufferManager.h" #include "tensorrt_llm/runtime/cudaEvent.h" -#include "tensorrt_llm/runtime/memoryCounters.h" #include "tensorrt_llm/runtime/runtimeKernels.h" #include @@ -209,6 +208,7 @@ void GptDecoderBatch::setup(executor::DecodingMode const& mode, SizeType32 maxBa mSinkTokenLength = sinkTokenLength; mMaxDecodingEngineTokens = maxTokensPerEngineStep; mFusedDecoder = fusedDecoder; + mDecodingMode = mode; TLLM_CHECK_WITH_INFO((mMaxDecodingEngineTokens == 1 && mSpeculativeDecodingMode.isNone()) || (mMaxDecodingEngineTokens > 1 && !mSpeculativeDecodingMode.isNone()), @@ -307,25 +307,18 @@ void GptDecoderBatch::setup(executor::DecodingMode const& mode, SizeType32 maxBa mMaxDecodingDecoderTokens = 1; } - if (!mFusedDecoder) - { - mStreams.resize(maxBatchSize); - auto const device = mStream->getDevice(); - for (SizeType32 i = 0; i < maxBatchSize; ++i) - { - mStreams[i] = std::make_shared(); - TLLM_CHECK(mStreams[i]->getDevice() == device); - } - } - auto const numOfDecoders = mFusedDecoder ? 1 : maxBatchSize; auto const maxBatchSizePerDecoder = mFusedDecoder ? maxBatchSize : 1; + auto const device = mStream->getDevice(); + mStreams.resize(numOfDecoders); mDecoders.resize(numOfDecoders); for (SizeType32 i = 0; i < numOfDecoders; ++i) { - auto& stream = mFusedDecoder ? mStream : mStreams.at(i); + mStreams[i] = std::make_shared(); + TLLM_CHECK(mStreams[i]->getDevice() == device); + mDecoders[i] = IGptDecoder::create(mode, dtype, maxBatchSizePerDecoder, maxBeamWidth, mVocabSize, - mVocabSizePadded, mMaxSequenceLength, stream, speculativeDecodingModulePtr); + mVocabSizePadded, mMaxSequenceLength, mStreams[i], speculativeDecodingModulePtr); } mNbSteps.clear(); @@ -400,13 +393,13 @@ void GptDecoderBatch::setupSpeculativeDecoding(ModelConfig const& modelConfig) } void GptDecoderBatch::newRequest( - SizeType32 batchIdx, decoder_batch::Request const& request, SamplingConfig const& samplingConfig) + SizeType32 batchSlot, decoder_batch::Request const& request, SamplingConfig const& samplingConfig) { TLLM_LOG_TRACE("%s start", __PRETTY_FUNCTION__); - TLLM_CHECK(batchIdx >= 0); + TLLM_CHECK(batchSlot >= 0); auto const& jointOutputIdsShape = mJointDecodingOutput->ids->getShape(); auto const batchSize = jointOutputIdsShape.d[0]; - TLLM_CHECK(0 <= batchSize && batchIdx < batchSize); + TLLM_CHECK(0 <= batchSize && batchSlot < batchSize); auto const maxBeamWidth = jointOutputIdsShape.d[1]; auto const beamWidth = samplingConfig.beamWidth; TLLM_CHECK_WITH_INFO(beamWidth <= maxBeamWidth, @@ -428,21 +421,21 @@ void GptDecoderBatch::newRequest( auto constexpr localBatchSize = 1; - auto const decoderIdx = mFusedDecoder ? 0 : batchIdx; - auto& stream = mFusedDecoder ? mStream : mStreams[decoderIdx]; + auto const decoderIdx = mFusedDecoder ? 0 : batchSlot; + auto const& stream = mStreams.at(decoderIdx); BufferManager manager{stream}; // input auto& dJointInput = *mJointDecodingInput; - auto& dInput = mDecodingInputs.at(batchIdx); + auto& dInput = mDecodingInputs.at(batchSlot); - TensorPtr endIdTensorPtr{ITensor::slice(constPointerCast(dJointInput.endIds), batchIdx, localBatchSize)}; + TensorPtr endIdTensorPtr{ITensor::slice(constPointerCast(dJointInput.endIds), batchSlot, localBatchSize)}; kernels::invokeFill(*endIdTensorPtr, endId, *stream); dInput = std::make_unique( inputLength, mMaxAttentionWindow, mSinkTokenLength, localBatchSize, dJointInput.logits, endIdTensorPtr); TensorPtr embeddingBiasSlice - = ITensor::slice(constPointerCast(dJointInput.embeddingBias), batchIdx, localBatchSize); + = ITensor::slice(constPointerCast(dJointInput.embeddingBias), batchSlot, localBatchSize); if (request.embeddingBias) { TLLM_CHECK(request.embeddingBias->getShape().nbDims == 2); @@ -462,20 +455,20 @@ void GptDecoderBatch::newRequest( auto setupWords = [fusedDecoder = mFusedDecoder](SharedConstPtr& inputWordsList, TensorPtr const& requestWordsList, SharedConstPtr& jointWordsPtrs, SharedConstPtr& jointWordsLens, SharedConstPtr& wordsPtrs, SharedConstPtr& wordsLens, SizeType32& inputMaxStopWordsLen, SizeType32& maxWordsLen, - SizeType32 localBatchSize, SizeType32 batchIdx) + SizeType32 localBatchSize, SizeType32 batchSlot) { if (requestWordsList) { auto const wordsLen = requestWordsList->getShape().d[1]; - BufferRange(*constPointerCast(jointWordsPtrs))[batchIdx] + BufferRange(*constPointerCast(jointWordsPtrs))[batchSlot] = bufferCast(*requestWordsList); - bufferCast(*constPointerCast(jointWordsLens))[batchIdx] = wordsLen; + bufferCast(*constPointerCast(jointWordsLens))[batchSlot] = wordsLen; // FIXME(nkorobov): this is monotonically growing size maxWordsLen = std::max(static_cast(wordsLen), maxWordsLen); if (!fusedDecoder) { - wordsPtrs = ITensor::slice(jointWordsPtrs, batchIdx, localBatchSize); - wordsLens = ITensor::slice(jointWordsLens, batchIdx, localBatchSize); + wordsPtrs = ITensor::slice(jointWordsPtrs, batchSlot, localBatchSize); + wordsLens = ITensor::slice(jointWordsLens, batchSlot, localBatchSize); inputMaxStopWordsLen = wordsLen; } // NOTE(nkorobov): dInput->WordsList is not used in gptDecoder, but required to keep WordsList's @@ -484,38 +477,38 @@ void GptDecoderBatch::newRequest( } else { - bufferCast(*constPointerCast(jointWordsLens))[batchIdx] = 0; + bufferCast(*constPointerCast(jointWordsLens))[batchSlot] = 0; inputMaxStopWordsLen = 0; } }; setupWords(dInput->stopWordsList, request.stopWordsList, dJointInput.stopWordsPtrs, dJointInput.stopWordsLens, dInput->stopWordsPtrs, dInput->stopWordsLens, dInput->maxStopWordsLen, mMaxStopWordsLen, localBatchSize, - batchIdx); + batchSlot); dJointInput.maxStopWordsLen = mMaxStopWordsLen; setupWords(dInput->badWordsList, request.badWordsList, dJointInput.badWordsPtrs, dJointInput.badWordsLens, - dInput->badWordsPtrs, dInput->badWordsLens, dInput->maxBadWordsLen, mMaxBadWordsLen, localBatchSize, batchIdx); + dInput->badWordsPtrs, dInput->badWordsLens, dInput->maxBadWordsLen, mMaxBadWordsLen, localBatchSize, batchSlot); dJointInput.maxBadWordsLen = mMaxBadWordsLen; TensorPtr sequenceLimitLength{ - ITensor::slice(constPointerCast(dJointInput.sequenceLimitLength), batchIdx, localBatchSize)}; + ITensor::slice(constPointerCast(dJointInput.sequenceLimitLength), batchSlot, localBatchSize)}; kernels::invokeFill(*sequenceLimitLength, inputLength + maxNewTokens, *stream); dInput->sequenceLimitLength = std::move(sequenceLimitLength); - TensorPtr inputLengths{ITensor::slice(constPointerCast(dJointInput.lengths), batchIdx, localBatchSize)}; + TensorPtr inputLengths{ITensor::slice(constPointerCast(dJointInput.lengths), batchSlot, localBatchSize)}; kernels::invokeFill(*inputLengths, inputLength, *stream); dInput->lengths = inputLengths; // output auto& dJointOutput = *mJointDecodingOutput; - auto& dOutput = mDecodingOutputs.at(batchIdx); + auto& dOutput = mDecodingOutputs.at(batchSlot); auto const outputIdsShape = ITensor::makeShape({localBatchSize, beamWidth, mMaxSequenceLength}); - TensorPtr outputIds = ITensor::slice(dJointOutput.ids, batchIdx, localBatchSize); + TensorPtr outputIds = ITensor::slice(dJointOutput.ids, batchSlot, localBatchSize); outputIds->reshape(outputIdsShape); dOutput = std::make_unique(outputIds); - dOutput->finishedSum = ITensor::slice(dJointOutput.finishedSum, batchIdx, localBatchSize); + dOutput->finishedSum = ITensor::slice(dJointOutput.finishedSum, batchSlot, localBatchSize); manager.setZero(*dOutput->finishedSum); dOutput->newTokensVec.resize(mMaxDecodingEngineTokens); @@ -523,16 +516,16 @@ void GptDecoderBatch::newRequest( { TensorPtr newTokensStepView = ITensor::slice(dJointOutput.newTokensSteps, ti, 1); newTokensStepView->squeeze(0); - dOutput->newTokensVec[ti] = ITensor::slice(newTokensStepView, batchIdx, localBatchSize); + dOutput->newTokensVec[ti] = ITensor::slice(newTokensStepView, batchSlot, localBatchSize); manager.setZero(*dOutput->newTokensVec[ti]); } // FIXME(nkorobov): we call setZero mMaxDecodingEngineTokens times for only 1 element for (SizeType32 ti = 0; ti < mMaxDecodingEngineTokens; ++ti) { - TensorPtr finishedStepsView = std::move(ITensor::slice(mFinishedSteps, ti, 1)); + TensorPtr finishedStepsView = ITensor::slice(mFinishedSteps, ti, 1); finishedStepsView->squeeze(0); - TensorPtr finishedSteps = std::move(ITensor::slice(finishedStepsView, batchIdx, localBatchSize)); + TensorPtr finishedSteps = ITensor::slice(finishedStepsView, batchSlot, localBatchSize); manager.setZero(*finishedSteps); } @@ -540,14 +533,14 @@ void GptDecoderBatch::newRequest( dOutput->cumLogProbs = nullptr; if ((samplingConfig.cumLogProbs.has_value() && samplingConfig.cumLogProbs->at(0)) || beamWidth > 1) { - dOutput->cumLogProbs = ITensor::slice(dJointOutput.cumLogProbs, batchIdx, localBatchSize); + dOutput->cumLogProbs = ITensor::slice(dJointOutput.cumLogProbs, batchSlot, localBatchSize); manager.setZero(*dOutput->cumLogProbs); } dOutput->logProbs = nullptr; if (samplingConfig.outputLogProbs.has_value() && samplingConfig.outputLogProbs->at(0)) { - dOutput->logProbs = ITensor::slice(dJointOutput.logProbs, batchIdx, localBatchSize); + dOutput->logProbs = ITensor::slice(dJointOutput.logProbs, batchSlot, localBatchSize); manager.setZero(*dOutput->logProbs); } @@ -555,10 +548,10 @@ void GptDecoderBatch::newRequest( { kernels::invokeFill( *IBuffer::slice(dOutput->cumLogProbs, 1, beamWidth - 1), DecodingOutput::kNegativeInfinity, *stream); - dOutput->parentIds = ITensor::slice(dJointOutput.parentIds, batchIdx, localBatchSize); + dOutput->parentIds = ITensor::slice(dJointOutput.parentIds, batchSlot, localBatchSize); dOutput->parentIds->reshape(outputIdsShape); manager.setZero(*dOutput->parentIds); - dOutput->beamHypotheses = dJointOutput.beamHypotheses.slice(batchIdx, localBatchSize); + dOutput->beamHypotheses = dJointOutput.beamHypotheses.slice(batchSlot, localBatchSize); dOutput->beamHypotheses.init(manager, endId); } @@ -566,7 +559,7 @@ void GptDecoderBatch::newRequest( if (numDecodingEngineTokens > 1) { TLLM_CHECK(beamWidth == 1); - newRequestSpeculativeDecoding(batchIdx, request, samplingConfig); + newRequestSpeculativeDecoding(batchSlot, request, samplingConfig); } // remaining @@ -574,12 +567,11 @@ void GptDecoderBatch::newRequest( { mDecoders[decoderIdx]->setup(samplingConfig, localBatchSize); } - TLLM_CHECK_WITH_INFO(!mFusedDecoder || beamWidth == 1, "Fused decoder is not supported for beam search yet."); - mBeamWidths[batchIdx] = beamWidth; - mNbSteps[batchIdx] = 0; - mFinished[batchIdx] = false; - mMaxNewTokens[batchIdx] = maxNewTokens; - mNumDecodingEngineTokens[batchIdx] = numDecodingEngineTokens; + mBeamWidths[batchSlot] = beamWidth; + mNbSteps[batchSlot] = 0; + mFinished[batchSlot] = false; + mMaxNewTokens[batchSlot] = maxNewTokens; + mNumDecodingEngineTokens[batchSlot] = numDecodingEngineTokens; // copy the request ids into outputIds auto const requestIdsShape = requestIds->getShape(); @@ -599,7 +591,7 @@ void GptDecoderBatch::newRequestSpeculativeDecoding( if (mSpeculativeDecodingMode.predictsDraftTokens()) { auto constexpr decoderIdx = 0; - auto& stream = mFusedDecoder ? mStream : mStreams[decoderIdx]; + auto const& stream = mStreams.at(decoderIdx); BufferManager manager{stream}; auto& dJointOutput = *mJointDecodingOutput; @@ -643,7 +635,7 @@ void GptDecoderBatch::newRequestDraftTokensExternal( TLLM_CHECK_WITH_INFO(mFusedDecoder, "Speculative decoding requires fused decoder"); auto constexpr decoderIdx = 0; - auto& stream = mFusedDecoder ? mStream : mStreams[decoderIdx]; + auto const& stream = mStreams.at(decoderIdx); BufferManager manager{stream}; auto constexpr localBatchSize = 1; @@ -654,12 +646,12 @@ void GptDecoderBatch::newRequestDraftTokensExternal( TensorPtr draftLogitsView = ITensor::view(request.draftLogits.value()); mAcceptByLogits[batchIdx] = true; - TensorPtr draftLogitsReqBatchSlice = std::move(ITensor::slice(mDraftLogits, batchIdx, localBatchSize)); + TensorPtr draftLogitsReqBatchSlice = ITensor::slice(mDraftLogits, batchIdx, localBatchSize); draftLogitsReqBatchSlice->squeeze(0); TensorPtr draftLogitsReqTokensSlice = ITensor::slice(draftLogitsReqBatchSlice, 0, numDraftTokens); manager.copy(*draftLogitsView, *draftLogitsReqTokensSlice); } - TensorPtr draftTokensReqBatchSlice = std::move(ITensor::slice(mDraftTokenIds, batchIdx, localBatchSize)); + TensorPtr draftTokensReqBatchSlice = ITensor::slice(mDraftTokenIds, batchIdx, localBatchSize); draftTokensReqBatchSlice->squeeze(0); TensorPtr draftTokensReqTokensSlice = ITensor::slice(draftTokensReqBatchSlice, 0, numDraftTokens); TensorPtr draftTokensView = ITensor::view(request.draftTokens, ITensor::makeShape({numDraftTokens})); @@ -688,7 +680,7 @@ void GptDecoderBatch::newRequestMedusa(SizeType32 batchIdx, decoder_batch::Reque TLLM_CHECK_WITH_INFO(mFusedDecoder, "Medusa requires fused decoder"); auto constexpr decoderIdx = 0; - auto& stream = mFusedDecoder ? mStream : mStreams[decoderIdx]; + auto const& stream = mStreams.at(decoderIdx); BufferManager manager{stream}; auto& dJointInput = *mJointDecodingInput; @@ -791,11 +783,26 @@ void GptDecoderBatch::newRequests(std::vector const& seqSlots, } if (mFusedDecoder) { - TensorPtr batchSlotsView = std::move(ITensor::slice(mBatchSlotsSetup, 0, localBatchSize)); + TensorPtr batchSlotsView = ITensor::slice(mBatchSlotsSetup, 0, localBatchSize); auto fusedSamplingConfig = SamplingConfig(samplingConfigs); - mDecoders[0]->setup(fusedSamplingConfig, localBatchSize, {batchSlotsView}, {*mJointDecodingOutput}); - } + mDecoders[0]->setup( + fusedSamplingConfig, localBatchSize, bufferCast(*batchSlotsView), {*mJointDecodingOutput}); + auto const& stream = mStreams.at(0); + CudaEvent event{}; + stream->record(event); + mStream->wait(event); + } + else + { + for (SizeType32 bi = 0; bi < localBatchSize; ++bi) + { + auto const& stream = mStreams.at(bi); + CudaEvent event{}; + stream->record(event); + mStream->wait(event); + } + } TLLM_LOG_TRACE("%s stop", __PRETTY_FUNCTION__); } @@ -868,7 +875,7 @@ void GptDecoderBatch::forwardUnfusedDecoder( continue; } - auto& stream = mFusedDecoder ? mStream : mStreams[bi]; + auto const& stream = mStreams.at(bi); if (async) { stream->wait(eventStart); @@ -922,7 +929,7 @@ void GptDecoderBatch::forwardUnfusedDecoder( { if (step == mNumDecodingEngineTokens[bi] - 1) { - auto& stream = mFusedDecoder ? mStream : mStreams[bi]; + auto const& stream = mStreams.at(bi); CudaEvent event{}; stream->record(event); mStream->wait(event); @@ -938,12 +945,13 @@ void GptDecoderBatch::forwardFusedDecoder( { TLLM_LOG_TRACE("%s start", __PRETTY_FUNCTION__); + auto eventStart = CudaEvent{}; + mStream->record(eventStart); + auto& allTargetLogits = input.logits; auto const& jointOutputIdsShape = mJointDecodingOutput->ids->getShape(); auto const maxBeamWidth = jointOutputIdsShape.d[1]; - TLLM_CHECK_WITH_INFO(maxBeamWidth == 1, "Fused decoder does not support beam search"); - auto constexpr singleRequest = 1; TLLM_CHECK(static_cast(output.sequenceLengths->getSize()) == mActualBatchSize * maxBeamWidth); @@ -952,13 +960,20 @@ void GptDecoderBatch::forwardFusedDecoder( = ITensor::view(output.sequenceLengths, ITensor::makeShape({mActualBatchSize, maxBeamWidth})); TLLM_CHECK(sequenceLengths); - auto batchSlotsDecoderPtr = bufferCast(*mBatchSlotsDecoder); + auto batchSlotsDecoderPtr + = input.seqSlots ? bufferCast(*input.seqSlots) : bufferCast(*mBatchSlotsDecoder); auto batchSlotsAcceptTokensPtr = bufferCast(*mBatchSlotsAcceptTokens); auto batchSlotsAcceptLogitsPtr = bufferCast(*mBatchSlotsAcceptLogits); auto& dInput = *mJointDecodingInput; auto& dOutput = *mJointDecodingOutput; auto& decoder = *mDecoders[0]; - auto& stream = mFusedDecoder ? mStream : mStreams[0]; + auto const& stream = mStreams.at(0); + + if (maxBeamWidth > 1) + { + dInput.cacheIndirection = input.cacheIndirection; + dOutput.cacheIndirection = output.cacheIndirection; + } if (mSpeculativeDecodingMode.isExplicitDraftTokens()) { @@ -967,6 +982,11 @@ void GptDecoderBatch::forwardFusedDecoder( bool const async = forwardType == ForwardType::kASYNC; + if (async) + { + stream->wait(eventStart.get()); + } + SizeType32 localBatchDecoderIdx = 0; SizeType32 localBatchAcceptTokensIdx = 0; SizeType32 localBatchAcceptLogitsIdx = 0; @@ -1010,7 +1030,7 @@ void GptDecoderBatch::forwardFusedDecoder( continue; } auto& targetLogits = allTargetLogits[bi]; - SharedConstPtr logitsSlice = std::move(ITensor::slice(targetLogits, step, singleRequest)); + SharedConstPtr logitsSlice = ITensor::slice(targetLogits, step, singleRequest); logitsVec.push_back(logitsSlice); targetLogitsPtrsSlicePtr[targetLogitsIdx++] = logitsSlice->data(); } @@ -1023,10 +1043,10 @@ void GptDecoderBatch::forwardFusedDecoder( float const randomAcceptanceThreshold = useRandomAcceptanceThreshold ? 0 : samplingConfig.draftAcceptanceThreshold.value()[0]; - TensorPtr batchSlotsAcceptLogitsStepSlice = std::move(ITensor::slice(mBatchSlotsAcceptLogits, step, 1)); + TensorPtr batchSlotsAcceptLogitsStepSlice = ITensor::slice(mBatchSlotsAcceptLogits, step, 1); batchSlotsAcceptLogitsStepSlice->squeeze(0); TensorPtr batchSlotsAcceptLogitsSlice - = std::move(ITensor::slice(batchSlotsAcceptLogitsStepSlice, 0, localBatchAcceptLogitsIdx)); + = ITensor::slice(batchSlotsAcceptLogitsStepSlice, 0, localBatchAcceptLogitsIdx); IGptDecoder::acceptDraftTokensByLogits( /* [maxBatchSize, maxDecodingTokens, vocabPadded] */ *mDraftLogits, @@ -1044,13 +1064,23 @@ void GptDecoderBatch::forwardFusedDecoder( TensorPtr finishedStepsOutput = ITensor::slice(mFinishedSteps, std::min(maxDecodingEngineTokens - 1, step + 1), 1); finishedStepsInput->squeeze(0); finishedStepsOutput->squeeze(0); - TensorPtr newTokensStepView = std::move(ITensor::slice(dOutput.newTokensSteps, step, mMaxDecodingDecoderTokens)); + TensorPtr newTokensStepView = ITensor::slice(dOutput.newTokensSteps, step, mMaxDecodingDecoderTokens); dInput.logitsVec = logitsVec; dInput.finished = finishedStepsInput; - TensorPtr batchSlotsDecoderSlice = std::move(ITensor::slice(mBatchSlotsDecoder, step, 1)); - batchSlotsDecoderSlice->squeeze(0); - dInput.batchSlots = batchSlotsDecoderSlice; + + if (input.seqSlots) + { + TensorPtr batchSlotsDecoderSlice = ITensor::slice(input.seqSlots, step, 1); + dInput.batchSlots = batchSlotsDecoderSlice; + } + else + { + TensorPtr batchSlotsDecoderSlice = ITensor::slice(mBatchSlotsDecoder, step, 1); + batchSlotsDecoderSlice->squeeze(0); + dInput.batchSlots = batchSlotsDecoderSlice; + } + dInput.batchSize = localBatchDecoderIdx; if (mSpeculativeDecodingMode.isMedusa()) { @@ -1088,7 +1118,7 @@ void GptDecoderBatch::forwardFusedDecoder( } if (async && localBatchAcceptTokensIdx > 0) { - TensorPtr batchSlotsAcceptTokensStepSlice = std::move(ITensor::slice(mBatchSlotsAcceptTokens, step, 1)); + TensorPtr batchSlotsAcceptTokensStepSlice = ITensor::slice(mBatchSlotsAcceptTokens, step, 1); batchSlotsAcceptTokensStepSlice->squeeze(0); auto batchSlotsAcceptTokensSlice = ITensor::slice(batchSlotsAcceptTokensStepSlice, 0, localBatchAcceptTokensIdx); @@ -1096,7 +1126,7 @@ void GptDecoderBatch::forwardFusedDecoder( // Update finished state for 0th step auto finishedFinal = ITensor::slice(mFinishedSteps, step, 1); IGptDecoder::acceptDraftTokensByIds( - /* [maxBatchSize, maxSeqLen] */ *dOutput.ids, + /* [maxBatchSize, maxBeamWidth, maxSeqLen] */ *dOutput.ids, /* [maxBatchSize, maxDecodingDraftTokens] */ *mDraftTokenIds, /* [maxBatchSize] */ *dInput.lengths, /* [maxBatchSize] */ *mNumDraftTokens, @@ -1112,6 +1142,7 @@ void GptDecoderBatch::forwardFusedDecoder( { CudaEvent event{}; stream->record(event); + mStream->wait(event); } TLLM_LOG_TRACE("%s stop", __PRETTY_FUNCTION__); @@ -1157,20 +1188,45 @@ void GptDecoderBatch::forwardSync( } // TODO call this at the end of forward if mFinished[i] changes from false to true? -CudaEvent GptDecoderBatch::postProcessRequest(SizeType32 batchIdx) const +CudaEvent GptDecoderBatch::postProcessRequest( + SizeType32 batchSlot, std::optional> samplingConfig) const { TLLM_LOG_TRACE("%s start", __PRETTY_FUNCTION__); - auto& stream = mFusedDecoder ? mStream : mStreams[batchIdx]; + + auto& stream = mFusedDecoder ? mStream : mStreams[batchSlot]; auto manager = BufferManager{stream}; - auto& decoder = *mDecoders[batchIdx]; + auto& decoder = mFusedDecoder ? *mDecoders[0] : *mDecoders[batchSlot]; - auto& dInput = *mDecodingInputs[batchIdx]; - auto& dOutput = *mDecodingOutputs[batchIdx]; + auto& dInput = *mDecodingInputs[batchSlot]; + auto& dOutput = *mDecodingOutputs[batchSlot]; + + if (mFusedDecoder) + { + auto& dJointOutput = *mJointDecodingOutput; + + auto slice = [&batchSlot](auto& a, auto& b) + { + if (b && b->getShape().d[0] > 0) + { + a = ITensor::slice(b, batchSlot, 1); + } + }; + + slice(dOutput.cacheIndirection, dJointOutput.cacheIndirection); + slice(dOutput.lengths, dJointOutput.lengths); + slice(dOutput.finished, dJointOutput.finished); + slice(dOutput.logProbs, dJointOutput.logProbs); + + dOutput.newTokens = ITensor::view(dJointOutput.newTokens); + TLLM_CHECK(dOutput.newTokens->getShape().d[0] == 1); + dOutput.newTokens->squeeze(0); + dOutput.newTokens = ITensor::slice(dOutput.newTokens, batchSlot, 1); + } // TODO can we do this inplace? auto& outputIds = dOutput.ids; auto finalOutputIds = manager.gpu(outputIds->getShape(), outputIds->getDataType()); - decoder.gatherTree(*finalOutputIds, dOutput, dInput, manager); + decoder.gatherTree(*finalOutputIds, dOutput, dInput, manager, samplingConfig); manager.copy(*finalOutputIds, *outputIds); CudaEvent event{}; @@ -1299,17 +1355,18 @@ void GptDecoderBatch::forwardSync() void GptDecoderBatch::finalize() const { TLLM_LOG_TRACE("%s start", __PRETTY_FUNCTION__); + auto batchSlots = bufferCast(*mBatchSlotsSetup); for (SizeType32 batchIdx = 0; batchIdx < mActualBatchSize; ++batchIdx) { - auto event = postProcessRequest(batchIdx); + auto event = postProcessRequest(batchSlots ? batchSlots[batchIdx] : batchIdx); } TLLM_LOG_TRACE("%s stop", __PRETTY_FUNCTION__); } -CudaEvent GptDecoderBatch::finalize(SizeType32 batchIdx) const +CudaEvent GptDecoderBatch::finalize(SizeType32 batchSlot, SamplingConfig const& samplingConfig) const { TLLM_LOG_TRACE("%s start", __PRETTY_FUNCTION__); - auto event = postProcessRequest(batchIdx); + auto event = postProcessRequest(batchSlot, samplingConfig); TLLM_LOG_TRACE("%s stop", __PRETTY_FUNCTION__); return event; } diff --git a/cpp/tensorrt_llm/runtime/gptJsonConfig.cpp b/cpp/tensorrt_llm/runtime/gptJsonConfig.cpp index f90bf004a..87fb874d6 100644 --- a/cpp/tensorrt_llm/runtime/gptJsonConfig.cpp +++ b/cpp/tensorrt_llm/runtime/gptJsonConfig.cpp @@ -184,7 +184,6 @@ void parseBuilderConfig(ModelConfig& modelConfig, Json const& builderConfig) auto const maxBeamWidth = parseJsonFieldOr(builderConfig, "max_beam_width", 0); auto const maxInputLen = parseJsonFieldOr(builderConfig, "max_input_len", 0); auto const maxSequenceLen = parseJsonFieldOr(builderConfig, "max_seq_len", 0); - auto const maxDraftLen = parseJsonFieldOr(builderConfig, "max_draft_len", 0); auto const maxNumTokens = parseJsonFieldOptional(builderConfig, "max_num_tokens"); auto const maxPromptEmbeddingTableSize = parseJsonFieldOr(builderConfig, "max_prompt_embedding_table_size", 0); @@ -198,7 +197,6 @@ void parseBuilderConfig(ModelConfig& modelConfig, Json const& builderConfig) modelConfig.setMaxInputLen(maxInputLen); modelConfig.setMaxSequenceLen(maxSequenceLen); modelConfig.setMaxNumTokens(maxNumTokens); - modelConfig.setMaxDraftLen(maxDraftLen); modelConfig.setMaxPromptEmbeddingTableSize(maxPromptEmbeddingTableSize); modelConfig.computeContextLogits(computeContextLogits); modelConfig.computeGenerationLogits(computeGenerationLogits); @@ -246,7 +244,6 @@ void parseLora(ModelConfig& modelConfig, Json const& json, Json const& pluginCon if (loraTargetModules.has_value()) { - modelConfig.setLoraModules(LoraModule::createLoraModules(loraTargetModules.value(), modelConfig.getHiddenSize(), modelConfig.getMlpHiddenSize(), modelConfig.getNbHeads(), modelConfig.getNbKvHeads(), modelConfig.getSizePerHead(), tensorParallelism)); @@ -366,7 +363,6 @@ GptJsonConfig parseJson(InputType&& input) // Speculative decoding module if (!engineVersionNone) { - SizeType32 maxDraftLen{0}; if (modelConfig.getSpeculativeDecodingMode().isExplicitDraftTokens()) { auto const& pretrainedConfig = json.at("pretrained_config"); @@ -374,7 +370,7 @@ GptJsonConfig parseJson(InputType&& input) // TODO(rkobus): adjust param names auto const maxNumPaths = parseJsonFieldOr(pretrainedConfig, "explicit_num_beams", 0); auto const maxDraftPathLen = parseJsonFieldOr(pretrainedConfig, "explicit_draft_len_per_beam", 0); - maxDraftLen = maxNumPaths * maxDraftPathLen; + auto const maxDraftLen = maxNumPaths * maxDraftPathLen; auto explicitDraftTokensModule = std::make_shared(maxDraftPathLen, maxDraftLen, maxNumPaths); @@ -384,7 +380,7 @@ GptJsonConfig parseJson(InputType&& input) else if (modelConfig.getSpeculativeDecodingMode().isMedusa()) { auto const& pretrainedConfig = json.at("pretrained_config"); - maxDraftLen = parseJsonFieldOr(pretrainedConfig, "max_draft_len", 0); + auto const maxDraftLen = parseJsonFieldOr(pretrainedConfig, "max_draft_len", 0); auto const medusaHeads = parseJsonFieldOptional(pretrainedConfig, "num_medusa_heads"); TLLM_CHECK_WITH_INFO(medusaHeads.has_value() && maxDraftLen > 0, "Both num_medusa_heads and max_draft_len have to be provided for Medusa model"); @@ -394,7 +390,7 @@ GptJsonConfig parseJson(InputType&& input) } else { - maxDraftLen = parseJsonFieldOr(builderConfig, "max_draft_len", 0); + auto const maxDraftLen = parseJsonFieldOr(builderConfig, "max_draft_len", 0); if (modelConfig.getSpeculativeDecodingMode().isLookaheadDecoding()) { TLLM_CHECK_WITH_INFO( @@ -405,10 +401,12 @@ GptJsonConfig parseJson(InputType&& input) else if (modelConfig.getSpeculativeDecodingMode().isDraftTokensExternal()) { TLLM_CHECK_WITH_INFO( - maxDraftLen, "max_draft_len has to be larger than 0 for decoding with external draft tokens"); + maxDraftLen > 0, "max_draft_len has to be larger than 0 for decoding with external draft tokens"); + auto speculativeDecodingModule + = std::make_shared(maxDraftLen, maxDraftLen, 1); + modelConfig.setSpeculativeDecodingModule(speculativeDecodingModule); } } - modelConfig.setMaxDraftLen(maxDraftLen); } // RNN config diff --git a/cpp/tensorrt_llm/runtime/tllmRuntime.cpp b/cpp/tensorrt_llm/runtime/tllmRuntime.cpp index 011d108b7..9894bf0ea 100644 --- a/cpp/tensorrt_llm/runtime/tllmRuntime.cpp +++ b/cpp/tensorrt_llm/runtime/tllmRuntime.cpp @@ -62,7 +62,7 @@ class StreamReader final : public nvinfer1::IStreamReader public: StreamReader(std::filesystem::path fp) { - mFile.open(fp.string()); + mFile.open(fp.string(), std::ios::binary | std::ios::in); TLLM_CHECK_WITH_INFO(mFile.good(), std::string("Error opening engine file: " + fp.string())); } diff --git a/cpp/tensorrt_llm/runtime/worldConfig.cpp b/cpp/tensorrt_llm/runtime/worldConfig.cpp index b5c2965aa..1ede9a2be 100644 --- a/cpp/tensorrt_llm/runtime/worldConfig.cpp +++ b/cpp/tensorrt_llm/runtime/worldConfig.cpp @@ -17,6 +17,7 @@ #include "tensorrt_llm/runtime/worldConfig.h" #include "tensorrt_llm/common/assert.h" +#include "tensorrt_llm/common/cudaUtils.h" #include "tensorrt_llm/common/logger.h" #include "tensorrt_llm/common/mpiUtils.h" #include "tensorrt_llm/common/stringUtils.h" @@ -92,12 +93,40 @@ WorldConfig WorldConfig::mpi(SizeType32 gpusPerNode, std::optional t auto& comm = COMM_SESSION; auto const mpiSize = comm.getSize(); auto const mpiRank = comm.getRank(); - TLLM_LOG_INFO("MPI size: %d, rank: %d", mpiSize, mpiRank); + auto const mpiLocalSize = LOCAL_COMM_SESSION.getSize(); + TLLM_LOG_INFO("MPI size: %d, MPI local size: %d, rank: %d", mpiSize, mpiLocalSize, mpiRank); auto const pp = pipelineParallelism.value_or(1); auto const tp = tensorParallelism.value_or(mpiSize / pp); - TLLM_LOG_DEBUG("TP: %d, PP: %d", tp, pp); - TLLM_CHECK(mpiSize == tp * pp); - TLLM_CHECK(mpiSize <= gpusPerNode || LOCAL_COMM_SESSION.getSize() == gpusPerNode); + TLLM_LOG_DEBUG("TP: %d, PP: %d, gpusPerNode: %d", tp, pp, gpusPerNode); + TLLM_CHECK_WITH_INFO(mpiSize == tp * pp, "MPI size %d != TP size %d * PP size %d", mpiSize, tp, pp); + SizeType32 deviceCount{0}; + TLLM_CUDA_CHECK(cudaGetDeviceCount(&deviceCount)); + if ((mpiSize < gpusPerNode && deviceCount < mpiSize) || (mpiSize >= gpusPerNode && deviceCount < gpusPerNode)) + { + TLLM_CHECK_WITH_INFO(deviceCount == 1, + "Detect %d GPUs, the GPU number is incompatible with %d gpusPerNode when MPI size is %d", deviceCount, + gpusPerNode, mpiSize); + TLLM_LOG_WARNING("gpusPerNode is %d but only detect single GPU, will set gpusPerNode to 1", gpusPerNode); + if (std::getenv("CUDA_VISIBLE_DEVICES") != nullptr || std::getenv("NVIDIA_VISIBLE_DEVICES") != nullptr) + { + std::ostringstream oss; + if (std::getenv("CUDA_VISIBLE_DEVICES") != nullptr) + { + oss << " CUDA_VISIBLE_DEVICES=" << std::getenv("CUDA_VISIBLE_DEVICES"); + } + if (std::getenv("NVIDIA_VISIBLE_DEVICES") != nullptr) + { + oss << " NVIDIA_VISIBLE_DEVICES=" << std::getenv("NVIDIA_VISIBLE_DEVICES"); + } + std::string envStr = oss.str(); + TLLM_LOG_WARNING( + "Detect%s, please provide the full device list instead of limiting to single device, " + "otherwise allreduce performance may be sub-optimal " + "since custom allreduce kernel relies on P2P access to peer devices.", + envStr); + } + gpusPerNode = 1; + } return WorldConfig{tp, pp, mpiRank, gpusPerNode, deviceIds}; #else diff --git a/cpp/tests/common/mpiUtilsTest.cpp b/cpp/tests/common/mpiUtilsTest.cpp index 93bd638ab..976017370 100644 --- a/cpp/tests/common/mpiUtilsTest.cpp +++ b/cpp/tests/common/mpiUtilsTest.cpp @@ -29,7 +29,16 @@ namespace mpi = tensorrt_llm::mpi; namespace tr = tensorrt_llm::runtime; -TEST(MPIUtils, RankAndSize) +TEST(MPIUtils, SessionRankAndSize) +{ + auto& session = mpi::MpiComm::session(); + auto const rank = session.getRank(); + EXPECT_LE(0, rank); + auto const size = session.getSize(); + EXPECT_LE(rank, size); +} + +TEST(MPIUtils, WorldRankAndSize) { auto& comm = mpi::MpiComm::world(); auto const rank = comm.getRank(); diff --git a/cpp/tests/kernels/fp8Gemm/fp8GemmKernelTest.cpp b/cpp/tests/kernels/fp8Gemm/fp8GemmKernelTest.cpp index 87c6fa0d2..6557aa3df 100644 --- a/cpp/tests/kernels/fp8Gemm/fp8GemmKernelTest.cpp +++ b/cpp/tests/kernels/fp8Gemm/fp8GemmKernelTest.cpp @@ -1,3 +1,4 @@ +#include #include #include #include @@ -178,7 +179,6 @@ void run_cpu(void* weight, void* activation, float scale, Params const& params, } } -template float run_cuda_kernel(Params& params, int warmup, int iter) { cudaStream_t s; @@ -188,12 +188,12 @@ float run_cuda_kernel(Params& params, int warmup, int iter) cudaEventCreate(&end); for (int i = 0; i < warmup; ++i) { - tensorrt_llm::kernels::fp8_gemm::fp8GemmLauncher(params, s); + tensorrt_llm::kernels::fp8_gemm::fp8GemmDispatcher(params, s); } cudaEventRecord(begin, s); for (int i = 0; i < iter; ++i) { - tensorrt_llm::kernels::fp8_gemm::fp8GemmLauncher(params, s); + tensorrt_llm::kernels::fp8_gemm::fp8GemmDispatcher(params, s); } cudaEventRecord(end, s); cudaEventSynchronize(end); @@ -317,11 +317,21 @@ template bool benchmark_and_verify(int m, int n, int k, tensorrt_llm::common::QuantMode const& quant_mode, int warmup, int iter, bool debug = false, bool run_cublas = false) { + constexpr nvinfer1::DataType kInputDatatype = std::is_same::value ? nvinfer1::DataType::kFLOAT + : std::is_same::value ? nvinfer1::DataType::kHALF + : std::is_same::value ? nvinfer1::DataType::kBF16 + : nvinfer1::DataType::kFP8; + + constexpr nvinfer1::DataType kOutputDatatype = std::is_same::value ? nvinfer1::DataType::kFLOAT + : std::is_same::value ? nvinfer1::DataType::kHALF + : std::is_same::value ? nvinfer1::DataType::kBF16 + : nvinfer1::DataType::kFP8; + std::srand(20240123); simple_assert(m <= 4); printf("mnk (%d, %d, %d), output %s\n", m, n, k, typeid(OutputType).name()); - CudaBuffer d_act(m * k); - CudaBuffer d_weight(k * n); + CudaBuffer d_act(m * k * sizeof(InputType)); + CudaBuffer d_weight(k * n * sizeof(InputType)); CudaBuffer d_out(m * n * sizeof(OutputType)); std::vector h_act(m * k); std::vector h_weight(k * n); @@ -342,12 +352,13 @@ bool benchmark_and_verify(int m, int n, int k, tensorrt_llm::common::QuantMode c d_act.copy_from(h_act.data()); d_weight.copy_from(h_weight.data()); - Params params{d_act.data(), d_weight.data(), h_alpha[0], d_out.data(), m, n, k, quant_mode}; + Params params{ + d_act.data(), d_weight.data(), h_alpha[0], d_out.data(), m, n, k, quant_mode, kInputDatatype, kOutputDatatype}; run_cpu(h_weight.data(), h_act.data(), h_alpha[0], params, h_out_gt.data()); float time1, time2; - time1 = run_cuda_kernel(params, warmup, iter); + time1 = run_cuda_kernel(params, warmup, iter); d_out.copy_to(h_out_cuda.data()); bool pass_cuda_kernel = compare(h_out_cuda.data(), h_out_gt.data(), m * n); @@ -378,7 +389,7 @@ bool benchmark_and_verify(int m, int n, int k, tensorrt_llm::common::QuantMode c } #ifdef ENABLE_FP8 -TEST(Kernel, Fp8Gemm) +TEST(Kernel, Fp8Gemv) { int const arch = tensorrt_llm::common::getSMVersion(); bool pass; @@ -406,3 +417,31 @@ TEST(Kernel, Fp8Gemm) } } #endif + +TEST(Kernel, Fp16Gemv) +{ + int const arch = tensorrt_llm::common::getSMVersion(); + bool pass; + int warmup = 10, iter = 30; + std::vector ms{1, 2, 3, 4}; + std::vector ns{2047, 2048, 4096}; + std::vector ks{2048, 4096}; + tensorrt_llm::common::QuantMode quant_mode = tensorrt_llm::common::QuantMode::fromQuantAlgo("FP8"); + for (auto m : ms) + { + for (auto n : ns) + { + for (auto k : ks) + { + pass = benchmark_and_verify(m, n, k, quant_mode, warmup, iter); + EXPECT_TRUE(pass); + pass = benchmark_and_verify(m, n, k, quant_mode, warmup, iter); + EXPECT_TRUE(pass); +#if defined(ENABLE_BF16) + pass = benchmark_and_verify<__nv_bfloat16, __nv_bfloat16>(m, n, k, quant_mode, warmup, iter); + EXPECT_TRUE(pass); +#endif + } + } + } +} diff --git a/cpp/tests/resources/data/test_model_lora_config.json b/cpp/tests/resources/data/test_model_lora_config.json index a592b302a..766f305bf 100644 --- a/cpp/tests/resources/data/test_model_lora_config.json +++ b/cpp/tests/resources/data/test_model_lora_config.json @@ -153,7 +153,6 @@ "reduce_fusion": false, "multi_block_mode": false, "enable_xqa": true, - "attention_qk_half_accumulation": false, "tokens_per_block": 64, "use_paged_context_fmha": false, "use_fp8_context_fmha": false, diff --git a/cpp/tests/resources/scripts/build_enc_dec_engines.py b/cpp/tests/resources/scripts/build_enc_dec_engines.py index 842d0d8cc..98d9544d2 100644 --- a/cpp/tests/resources/scripts/build_enc_dec_engines.py +++ b/cpp/tests/resources/scripts/build_enc_dec_engines.py @@ -129,7 +129,7 @@ def command(self): f"--output_dir {join(engine_dir, 'encoder')}", f'--paged_kv_cache disable', f'--moe_plugin disable', f'--enable_xqa disable', f'--max_beam_width {args.beams}', - f'--max_batch_size 8', f'--max_seq_len 1224', + f'--max_batch_size 8', f'--max_input_len 512', f'--gemm_plugin {args.dtype}', f'--bert_attention_plugin {args.dtype}', f'--gpt_attention_plugin {args.dtype}', @@ -143,7 +143,7 @@ def command(self): f'--paged_kv_cache enable', f'--moe_plugin disable', f'--enable_xqa disable', f'--max_beam_width {args.beams}', f'--max_batch_size 8', f'--max_seq_len 201', - f'--gemm_plugin {args.dtype}', + f'--max_encoder_input_len 512', f'--gemm_plugin {args.dtype}', f'--bert_attention_plugin {args.dtype}', f'--gpt_attention_plugin {args.dtype}', f'--remove_input_padding enable', f'--context_fmha disable', diff --git a/cpp/tests/resources/scripts/generate_expected_gptj_output.py b/cpp/tests/resources/scripts/generate_expected_gptj_output.py index 9a5a7943c..4bf5aa70b 100755 --- a/cpp/tests/resources/scripts/generate_expected_gptj_output.py +++ b/cpp/tests/resources/scripts/generate_expected_gptj_output.py @@ -23,7 +23,7 @@ def generate_output(engine: str, num_beams: int, output_name: str, - max_output_len: int = 8): + max_output_len: int = 4): tp_size = 1 pp_size = 1 diff --git a/cpp/tests/resources/scripts/test_cpp.py b/cpp/tests/resources/scripts/test_cpp.py index 82b19925f..07820fcae 100755 --- a/cpp/tests/resources/scripts/test_cpp.py +++ b/cpp/tests/resources/scripts/test_cpp.py @@ -595,6 +595,8 @@ def run_benchmarks(python_exe: str, root_dir: _pl.Path, build_dir: _pl.Path, run_command(benchmark, cwd=root_dir, timeout=600) req_rate_benchmark = benchmark + ["--request_rate", "100"] run_command(req_rate_benchmark, cwd=root_dir, timeout=600) + concurrency_benchmark = benchmark + ["--concurrency", "30"] + run_command(concurrency_benchmark, cwd=root_dir, timeout=600) benchmark = [ str(benchmark_exe_dir / "gptManagerBenchmark"), "--engine_dir", diff --git a/docs/source/advanced/gpt-runtime.md b/docs/source/advanced/gpt-runtime.md index 65d76500e..dca45221e 100644 --- a/docs/source/advanced/gpt-runtime.md +++ b/docs/source/advanced/gpt-runtime.md @@ -133,7 +133,7 @@ value for a given parameter, the vector can be limited to a single element * `frequencyPenalty`, a vector of float-point numbers to penalize tokens already present in the sequence (dependent on the number of appearances). It is additive penalty. It can have any value, values `< 0.0f` encourage repetition, `> 0.0f` discourage it. The default value is `0.0f`(no effect). - * `noRepeatNgramSize`, a vector of integers. If set to int > 0, all ngrams of that size can only occur once. + * `noRepeatNgramSize`, a vector of integers. It can have any value `> 0`. If set to int `> 0`, all ngrams of that size can only occur once. The parameters `repetitionPenalty`, `presencePenalty`, and `frequencyPenalty` are not mutually exclusive. diff --git a/docs/source/advanced/weight-streaming.md b/docs/source/advanced/weight-streaming.md index 24bdf9594..513cc1b10 100644 --- a/docs/source/advanced/weight-streaming.md +++ b/docs/source/advanced/weight-streaming.md @@ -6,7 +6,7 @@ TensorRT Weight Streaming can offload some weights to the CPU memory and stream This can reduce the weights size in GPU memory, therefore, we can run larger models or larger batch sizes in the same GPU memory budget. -During build time, build the engine with `--weight-streaming --gemm_plugin disable` since Weight Streaming only supports strongly typed models and non-plugin weights. During runtime, run with `--gpu_weights_percent x` to config the percent of weights that remained on the GPU. `x` can be a value from `0.0` to `1.0`. +During build time, build the engine with `--weight-streaming --gemm_plugin disable` since Weight Streaming only supports non-plugin weights. During runtime, run with `--gpu_weights_percent x` to config the percent of weights that remained on the GPU. `x` can be a value from `0.0` to `1.0`. Here is an example to run llama-7b with Weight Streaming: ```bash diff --git a/docs/source/installation/build-from-source-windows.md b/docs/source/installation/build-from-source-windows.md index bd75d8a91..4cca95bc6 100644 --- a/docs/source/installation/build-from-source-windows.md +++ b/docs/source/installation/build-from-source-windows.md @@ -11,7 +11,7 @@ This section is for advanced users. Skip this section if you plan to use the pre 1. Install prerequisites listed in our [Installing on Windows](https://nvidia.github.io/TensorRT-LLM/installation/windows.html) document. 2. Install [CMake](https://cmake.org/download/), version 3.27.7 is recommended, and select the option to add it to the system path. 3. Download and install [Visual Studio 2022](https://visualstudio.microsoft.com/). -4. Download and unzip [TensorRT 10.0.1.6](https://developer.nvidia.com/downloads/compute/machine-learning/tensorrt/10.0.1/zip/TensorRT-10.0.1.6.Windows10.win10.cuda-12.4.zip). +4. Download and unzip [TensorRT 10.1.0.27](https://developer.nvidia.com/downloads/compute/machine-learning/tensorrt/10.1.0/zip/TensorRT-10.1.0.27.Windows.win10.cuda-12.4.zip). ## Building a TensorRT-LLM Docker Image @@ -65,7 +65,7 @@ git submodule update --init --recursive 2. Build TensorRT-LLM. This command generates `build\tensorrt_llm-*.whl`. ```bash -python .\scripts\build_wheel.py -a "89-real" --trt_root C:\workspace\TensorRT-10.0.1.6\ +python .\scripts\build_wheel.py -a "89-real" --trt_root C:\workspace\TensorRT-10.1.0.27\ ``` 3. Copy or move `build\tensorrt_llm-*.whl` into your mounted folder so it can be accessed on your host machine. If you intend to use the C++ runtime, you'll also need to gather various DLLs from the build into your mounted folder. For more information, refer to [C++ Runtime Usage](#c-runtime-usage). @@ -77,7 +77,7 @@ python .\scripts\build_wheel.py -a "89-real" --trt_root C:\workspace\TensorRT-10 **Prerequisites** 1. Install all prerequisites (`git`, `python`, `CUDA`) listed in our [Installing on Windows](https://nvidia.github.io/TensorRT-LLM/installation/windows.html) document. -2. Install Nsight NVTX. TensorRT-LLM on Windows currently depends on NVTX assets that do not come packaged with the CUDA 12.4 installer. To install these assets, download the [CUDA 11.8 Toolkit](https://developer.nvidia.com/cuda-11-8-0-download-archive?target_os=Windows&target_arch=x86_64). +2. Install Nsight NVTX. TensorRT-LLM on Windows currently depends on NVTX assets that do not come packaged with the CUDA 12.4.1 installer. To install these assets, download the [CUDA 11.8 Toolkit](https://developer.nvidia.com/cuda-11-8-0-download-archive?target_os=Windows&target_arch=x86_64). 1. During installation, select **Advanced installation**. @@ -103,7 +103,7 @@ python .\scripts\build_wheel.py -a "89-real" --trt_root C:\workspace\TensorRT-10 1. Install [CMake](https://cmake.org/download/), version 3.27.7 is recommended, and select the option to add it to the system path. 2. Download and install [Visual Studio 2022](https://visualstudio.microsoft.com/). When prompted to select more Workloads, check **Desktop development with C++**. - 3. Download and unzip [TensorRT 10.0.1.6](https://developer.nvidia.com/downloads/compute/machine-learning/tensorrt/10.0.1/zip/TensorRT-10.0.1.6.Windows10.win10.cuda-12.4.zip). Move the folder to a location you can reference later, such as `%USERPROFILE%\inference\TensorRT`. + 3. Download and unzip [TensorRT 10.1.0.27](https://developer.nvidia.com/downloads/compute/machine-learning/tensorrt/10.1.0/zip/TensorRT-10.1.0.27.Windows.win10.cuda-12.4.zip). Move the folder to a location you can reference later, such as `%USERPROFILE%\inference\TensorRT`. 1. Add the libraries for TensorRT to your system's `Path` environment variable. Your `Path` should include a line like this: diff --git a/docs/source/installation/windows.md b/docs/source/installation/windows.md index dbef47a29..a03859312 100644 --- a/docs/source/installation/windows.md +++ b/docs/source/installation/windows.md @@ -15,7 +15,7 @@ We recommend checking out the [v0.10.0 tag](https://github.com/NVIDIA/TensorRT-L 1. Install all dependencies together. - 1. Run the provided PowerShell script `setup_env.ps1` located under the `/windows/` folder which installs Python and CUDA 12.4 automatically with default settings. Run PowerShell as Administrator to use the script. + 1. Run the provided PowerShell script `setup_env.ps1` located under the `/windows/` folder which installs Python and CUDA 12.4.1 automatically with default settings. Run PowerShell as Administrator to use the script. ```bash ./setup_env.ps1 [-skipCUDA] [-skipPython] @@ -30,7 +30,7 @@ We recommend checking out the [v0.10.0 tag](https://github.com/NVIDIA/TensorRT-L 1. Select **Add python.exe to PATH** at the start of the installation. The installation may only add the `python` command, but not the `python3` command. 2. Navigate to the installation path `%USERPROFILE%\AppData\Local\Programs\Python\Python310` (`AppData` is a hidden folder) and copy `python.exe` to `python3.exe`. - 2. Install [CUDA 12.4 Toolkit](https://developer.nvidia.com/cuda-12-4-0-download-archive?target_os=Windows&target_arch=x86_64). Use the Express Installation option. Installation may require a restart. + 2. Install [CUDA 12.4.1 Toolkit](https://developer.nvidia.com/cuda-12-4-1-download-archive?target_os=Windows&target_arch=x86_64). Use the Express Installation option. Installation may require a restart. **Steps** diff --git a/docs/source/media/picture-07-02-2024.png b/docs/source/media/picture-07-02-2024.png new file mode 100644 index 0000000000000000000000000000000000000000..bfe11e8c9784fe7816bb458c0c5166cd8916d42e GIT binary patch literal 689064 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

iv&}+?^c*W|QDCg>n)v%K}<1!1XBr&ICSL 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 z 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 z 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�@S? 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 z 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�(_mR2CZub?YO>O@T2M)V)I`sV><{BiPg~ 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 literal 0 HcmV?d00001 diff --git a/docs/source/performance/perf-best-practices.md b/docs/source/performance/perf-best-practices.md index 100c7d2e4..1a0a0fa5b 100644 --- a/docs/source/performance/perf-best-practices.md +++ b/docs/source/performance/perf-best-practices.md @@ -142,6 +142,10 @@ to the runtime, and will possibly be removed in the future releases. We have sup an auto fallback mechanism so that native NCCL kernel is used when hardware requirements are not satisfied to get the best performance. +If you use `CUDA_VISIBLE_DEVICES` or `NVIDIA_VISIBLE_DEVICES`, please provide the full device list +instead of limiting to single device, otherwise custom all reduce will be disabled since its kernels +rely on P2P access to peer devices, which is not allowed when only a single device is visible. + In addition, there is an experimental feature called "Reduce Norm Fusion" available to extend the custom AllReduce functionality. It can be enabled by using the `--reduce_fusion enable` argument with `trtllm-build` when the diff --git a/docs/source/reference/precision.md b/docs/source/reference/precision.md index df1ea5731..981810d72 100644 --- a/docs/source/reference/precision.md +++ b/docs/source/reference/precision.md @@ -149,7 +149,7 @@ This release of TensorRT-LLM contains the following examples: | SantaCoder | Y | Y | Y | . | . | Y | Y | . | . | | Skywork | Y | Y | Y | . | . | . | . | . | . | | StarCoder1 | Y | Y | Y | . | . | Y | Y | . | . | -| StarCoder2 | Y | Y | Y | . | . | Y | Y | . | . | +| StarCoder2 | Y | Y | Y | Y | . | Y | Y | . | . | | T5 | Y | Y | Y | . | . | . | . | . | . | | Whisper | Y | Y | Y | . | . | Y | Y | . | . | | BLIP2-OPT | Y | Y | Y | . | . | . | . | . | . | diff --git a/docs/source/reference/support-matrix.md b/docs/source/reference/support-matrix.md index 3ad64739d..12b75cdf6 100644 --- a/docs/source/reference/support-matrix.md +++ b/docs/source/reference/support-matrix.md @@ -104,7 +104,7 @@ The following table shows the supported software for TensorRT-LLM. - [Kosmos](https://github.com/NVIDIA/TensorRT-LLM/tree/main/examples/multimodal) - [LLaVA-v1.5](https://github.com/NVIDIA/TensorRT-LLM/tree/main/examples/multimodal) - [NeVA](https://github.com/NVIDIA/TensorRT-LLM/tree/main/examples/multimodal) - - [Nougat](https://github.com/NVIDIA/TensorRT-LLM/tree/main/examples/multimodal) Nougat-small, Nougat-base + - [Nougat](https://github.com/NVIDIA/TensorRT-LLM/tree/main/examples/multimodal) - [Phi-3-vision](https://github.com/NVIDIA/TensorRT-LLM/tree/main/examples/multimodal) - [Video NeVA](https://github.com/NVIDIA/TensorRT-LLM/tree/main/examples/multimodal) - [VILA](https://github.com/NVIDIA/TensorRT-LLM/tree/main/examples/multimodal) @@ -114,7 +114,7 @@ The following table shows the supported software for TensorRT-LLM. (2) INT4 AWQ and GPTQ are not supported on SM < 75.
(3) INT4 AWQ and GPTQ with FP8 activations require SM >= 89.
(4) [Encoder-Decoder](https://github.com/NVIDIA/TensorRT-LLM/tree/main/main/examples/enc_dec) provides general encoder-decoder functionality that supports many encoder-decoder models such as T5 family, BART family, Whisper family, NMT family, and so on. -(5) Multi-modal provides general multi-modal functionality that supports many multi-modal architectures such as BLIP family, LLaVA family, and so on. +(5) Multi-modal provides general multi-modal functionality that supports many multi-modal architectures such as BLIP2 family, LLaVA family, and so on. (6) Only supports bfloat16 precision. diff --git a/examples/apps/README.md b/examples/apps/README.md index 4fc6884d8..8ae4bc7f8 100644 --- a/examples/apps/README.md +++ b/examples/apps/README.md @@ -4,25 +4,29 @@ [chat.py](./chat.py) provides a small examples to play around with your model. You can run it with -`python3 examples/apps/chat.py ` -or -`mpirun -n python3 examples/apps/chat.py ` +`python3 ./chat.py --model --tokenizer --tp_size ` -You can modify prompt setting by entering options starting with '!!'. Type '!!help' to see available commands. +Please run `python3 ./chat.py --help` for more information on the arguments. + +Note that, the `model_dir` could accept the following formats: + +1. A path to a built TRT-LLM engine +2. A path to a local HuggingFace model +3. The name of a HuggingFace model such as "TinyLlama/TinyLlama-1.1B-Chat-v1.0" ## FastAPI server ### Install the additional requirements -` pip install -r examples/apps/requirements.txt` +` pip install -r ./requirements.txt` ### Start the server Suppose you have build an engine with `trtllm-build`, you can now serve it with: -`python3 -m examples.apps.fastapi_server &` -or -`mpirun -n python3 -m examples.server.server &` +`python3 ./fastapi_server &` + +To get more information on all the arguments, please run `python3 ./fastapi_server --help`. ### Send requests diff --git a/examples/apps/chat.py b/examples/apps/chat.py old mode 100644 new mode 100755 index 7ccb6648e..0285d98f5 --- a/examples/apps/chat.py +++ b/examples/apps/chat.py @@ -1,60 +1,98 @@ #! /usr/bin/env python3 -import argparse import code -import readline # NOQA -from argparse import ArgumentParser -from pathlib import Path -from tensorrt_llm.executor import GenerationExecutorWorker +import click +import colorama +from transformers import AutoTokenizer, PreTrainedTokenizer +from tensorrt_llm.hlapi import LLM, BuildConfig, KvCacheConfig, SamplingParams -class LLMChat(code.InteractiveConsole): - def __init__(self, executor): - super().__init__() - self.executor = executor - self.generation_kwargs = { - "max_new_tokens": 100, - "repetition_penalty": 1.05, - } - self.parser = ArgumentParser(prefix_chars="!") - self.parser.add_argument("!!max_new_tokens", type=int) - self.parser.add_argument("!!repetition_penalty", type=float) +class LlmConsole(code.InteractiveConsole): + + def __init__(self, + llm: LLM, + tokenizer: PreTrainedTokenizer, + sampling_params: SamplingParams, + locals=None): + super().__init__(locals=locals) + self.llm = llm + self.tokenizer = tokenizer + + self.sampling_params = sampling_params + + self.history = [] def runsource(self, source: str, filename: str = "", symbol: str = "single") -> bool: - del filename, symbol - - if source.startswith("!"): - args = self.parser.parse_args(source.split(" ")) - for k, v in vars(args).items(): - if v is not None: - self.generation_kwargs[k] = v - return False - - future = self.executor.generate_async(source, - streaming=True, - **self.generation_kwargs) - for partial_result in future: - print(partial_result.text_diff, end="") - print("") + prompt = source.strip() + if prompt == "quit": + self.llm.__exit__(None, None, None) + return True # exit the console + + message = {"role": "user", "content": prompt} + self.history.append(message) + + input = self.tokenizer.apply_chat_template(self.history, + add_generation_prompt=True) + output = self.llm.generate([input], + sampling_params=self.sampling_params)[0] + generation = self.tokenizer.decode(output.outputs[0].token_ids, + skip_special_tokens=True) + print(colorama.Fore.CYAN + "AI: " + colorama.Style.RESET_ALL + + generation.strip()) + print() + + self.history.append({ + "role": "assistant", + "content": generation.strip() + }) return False -def main(model_dir: Path, tokenizer: Path | str): +@click.command() +@click.option( + "--model", + required=True, + help= + "The model to use, either a path to a model or a model name from Hugging Face's model hub." +) +@click.option("--tokenizer", default=None, help="The tokenizer to use") +@click.option("--tp_size", + default=1, + help="The number of devices for tensor parallelism to use") +def main(model: str, tokenizer: str, tp_size: int): + kv_cache_config = KvCacheConfig( + # you can also set max_tokens instead + free_gpu_memory_fraction=0.8) + kv_cache_config.enable_block_reuse = True + + build_config = BuildConfig() + build_config.max_batch_size = 1 + build_config.max_input_len = 6000 + build_config.max_num_tokens = 10240 + + sampling_params = SamplingParams() + sampling_params.beam_width = 1 + sampling_params.max_new_tokens = 100 + sampling_params.temperature = 0.5 + sampling_params.top_p = 0.95 + + llm = LLM(model, + build_config=build_config, + kv_cache_config=kv_cache_config, + tensor_parallel_size=tp_size) + + hf_tokenizer = AutoTokenizer.from_pretrained(tokenizer or model) - with GenerationExecutorWorker(model_dir, tokenizer, 1) as executor: - executor.block_subordinates() - repl = LLMChat(executor) - repl.interact(banner="", exitmsg="") + console = LlmConsole(llm, + tokenizer=hf_tokenizer, + sampling_params=sampling_params) + console.interact(banner="Welcome to LLM Console!", exitmsg="Goodbye!") -if __name__ == "__main__": - parser = argparse.ArgumentParser() - parser.add_argument("model_dir", type=Path) - parser.add_argument("tokenizer", type=Path) - args = parser.parse_args() - main(args.model_dir, args.tokenizer) +if __name__ == '__main__': + main() diff --git a/examples/apps/fastapi_server.py b/examples/apps/fastapi_server.py old mode 100644 new mode 100755 index 45b123c92..972cb90d6 --- a/examples/apps/fastapi_server.py +++ b/examples/apps/fastapi_server.py @@ -1,82 +1,112 @@ -import argparse +#!/usr/bin/env python import asyncio import json +import logging from typing import AsyncGenerator, Optional +import click import uvicorn from fastapi import FastAPI, Request from fastapi.responses import JSONResponse, Response, StreamingResponse -from tensorrt_llm.executor import GenerationExecutorWorker, GenerationResult +from tensorrt_llm.hlapi import LLM, BuildConfig, KvCacheConfig, SamplingParams TIMEOUT_KEEP_ALIVE = 5 # seconds. -TIMEOUT_TO_PREVENT_DEADLOCK = 1 # seconds. -app = FastAPI() -executor: Optional[GenerationExecutorWorker] = None -@app.get("/stats") -async def stats() -> Response: - assert executor is not None - return JSONResponse(json.loads(await executor.aget_stats())) +class LlmServer: + def __init__(self, llm: LLM, sampling_params: SamplingParams, + kv_cache_config: KvCacheConfig): + self.llm = llm + self.sampling_params = sampling_params + self.kv_cache_config = kv_cache_config -@app.get("/health") -async def health() -> Response: - """Health check.""" - return Response(status_code=200) + self.app = FastAPI() + self.register_routes() + def register_routes(self): + self.app.add_api_route("/stats", self.stats, methods=["GET"]) + self.app.add_api_route("/health", self.health, methods=["GET"]) + self.app.add_api_route("/generate", self.generate, methods=["POST"]) -@app.post("/generate") -async def generate(request: Request) -> Response: - assert executor is not None - """Generate completion for the request. + async def stats(self) -> Response: + content = await self.llm.aget_stats() + return JSONResponse(json.loads(content)) - The request should be a JSON object with the following fields: - - prompt: the prompt to use for the generation. - - stream: whether to stream the results or not. - - other fields: the sampling parameters (See `SamplingParams` for details). - """ - request_dict = await request.json() + async def health(self) -> Response: + return Response(status_code=200) - prompt = request_dict.pop("prompt", "") - streaming = request_dict.pop("streaming", False) - promise = executor.generate_async(prompt, streaming, **request_dict) - assert isinstance(promise, GenerationResult) + async def generate(self, request: Request) -> Response: + ''' Generate completion for the request. - async def stream_results() -> AsyncGenerator[bytes, None]: - async for output in promise: - yield (json.dumps(output.text_diff) + "\0").encode("utf-8") + The request should be a JSON object with the following fields: + - prompt: the prompt to use for the generation. + - stream: whether to stream the results or not. + - other fields: the sampling parameters (See `SamplingParams` for details). + ''' + request_dict = await request.json() - if streaming: - return StreamingResponse(stream_results()) + prompt = request_dict.pop("prompt", "") + streaming = request_dict.pop("streaming", False) + assert not request_dict, f"Unexpected fields in request: {request_dict}" - # Non-streaming case - await promise.aresult() - return JSONResponse({"text": promise.text}) + sampling_params = SamplingParams(**request_dict) + promise = self.llm.generate_async(prompt, + streaming=streaming, + sampling_params=sampling_params) -async def main(args): - global executor + async def stream_results() -> AsyncGenerator[bytes, None]: + async for output in promise: + yield output.outputs[0].text_diff.encode("utf-8") - with GenerationExecutorWorker(args.model_dir, args.tokenizer_type, - args.max_beam_width) as executor: - executor.block_subordinates() - config = uvicorn.Config(app, - host=args.host, - port=args.port, + if streaming: + return StreamingResponse(stream_results()) + + # Non-streaming case + await promise.aresult() + return JSONResponse({"text": promise.outputs[0].text}) + + async def __call__(self, host, port): + config = uvicorn.Config(self.app, + host=host, + port=port, log_level="info", timeout_keep_alive=TIMEOUT_KEEP_ALIVE) await uvicorn.Server(config).serve() +@click.command() +@click.argument("model_dir") +@click.option("--host", type=str, default=None) +@click.option("--port", type=int, default=8000) +@click.option("--max_beam_width", type=int, default=1) +@click.option("--tp_size", type=int, default=1) +def entrypoint(model_dir: str, + host: Optional[str] = None, + port: int = 8000, + max_beam_width: int = 1, + tp_size: int = 1): + host = host or "0.0.0.0" + port = port or 8000 + logging.info(f"Starting server at {host}:{port}") + + build_config = BuildConfig(max_batch_size=10, max_beam_width=max_beam_width) + + llm = LLM(model_dir, + tensor_parallel_size=tp_size, + build_config=build_config) + + sampling_params = SamplingParams(max_new_tokens=1024) + kv_cache_config = KvCacheConfig(free_gpu_memory_fraction=0.8) + + server = LlmServer(llm=llm, + sampling_params=sampling_params, + kv_cache_config=kv_cache_config) + + asyncio.run(server(host, port)) + + if __name__ == "__main__": - parser = argparse.ArgumentParser() - parser.add_argument("model_dir") - parser.add_argument("tokenizer_type") - parser.add_argument("--host", type=str, default=None) - parser.add_argument("--port", type=int, default=8000) - parser.add_argument("--max_beam_width", type=int, default=1) - args = parser.parse_args() - - asyncio.run(main(args)) + entrypoint() diff --git a/examples/apps/requirements.txt b/examples/apps/requirements.txt index 97dc7cd8c..59c3de5ff 100644 --- a/examples/apps/requirements.txt +++ b/examples/apps/requirements.txt @@ -1,2 +1,3 @@ fastapi uvicorn +colorama diff --git a/examples/baichuan/requirements.txt b/examples/baichuan/requirements.txt index 89b722b3e..663a3c11b 100644 --- a/examples/baichuan/requirements.txt +++ b/examples/baichuan/requirements.txt @@ -1,5 +1,5 @@ --extra-index-url https://pypi.nvidia.com -tensorrt_llm==0.11.0.dev2024062500 +tensorrt_llm==0.12.0.dev2024070200 datasets~=2.15.0 evaluate~=0.4.1 rouge_score~=0.1.2 diff --git a/examples/bert/README.md b/examples/bert/README.md index bd2d7e4ee..69e33b2f3 100644 --- a/examples/bert/README.md +++ b/examples/bert/README.md @@ -32,9 +32,6 @@ python3 build.py --model RobertaModel --dtype=float16 --log_level=verbose # Build BertModel with TensorRT-LLM BERT Attention plugin for enhanced runtime performance python3 build.py --dtype=float16 --log_level=verbose --use_bert_attention_plugin float16 - -# Build RobertaForSequenceClassification with half-precision accumulation for attention BMM1 (applied to unfused MHA plugins) -python3 build.py --model RobertaForSequenceClassification --dtype=float16 --log_level=verbose --use_bert_attention_plugin float16 --enable_qk_half_accum ``` The following command can be used to run the model on a single GPU: diff --git a/examples/bert/build.py b/examples/bert/build.py index df72c102c..2b081e25b 100644 --- a/examples/bert/build.py +++ b/examples/bert/build.py @@ -82,9 +82,6 @@ def parse_arguments(): type=str, default=False, choices=['float16', 'float32']) - parser.add_argument('--enable_qk_half_accum', - default=False, - action='store_true') parser.add_argument('--enable_context_fmha', default=False, action='store_true') @@ -235,8 +232,6 @@ def parse_arguments(): network.plugin_config.bert_attention_plugin = args.use_bert_attention_plugin if args.use_gemm_plugin: network.plugin_config.gemm_plugin = args.use_gemm_plugin - if args.enable_qk_half_accum: - network.plugin_config.attention_qk_half_accumulation = True assert not (args.enable_context_fmha and args.enable_context_fmha_fp32_acc) if args.enable_context_fmha: network.plugin_config.set_context_fmha(ContextFMHAType.enabled) diff --git a/examples/bloom/requirements.txt b/examples/bloom/requirements.txt index 5626d3984..19db45403 100644 --- a/examples/bloom/requirements.txt +++ b/examples/bloom/requirements.txt @@ -1,5 +1,5 @@ --extra-index-url https://pypi.nvidia.com -tensorrt_llm==0.11.0.dev2024062500 +tensorrt_llm==0.12.0.dev2024070200 datasets~=2.14.5 evaluate~=0.4.1 rouge_score~=0.1.2 diff --git a/examples/chatglm/requirements.txt b/examples/chatglm/requirements.txt index 3236169a0..ef30de859 100644 --- a/examples/chatglm/requirements.txt +++ b/examples/chatglm/requirements.txt @@ -1,5 +1,5 @@ --extra-index-url https://pypi.nvidia.com -tensorrt_llm==0.11.0.dev2024062500 +tensorrt_llm==0.12.0.dev2024070200 datasets~=2.14.5 evaluate~=0.4.1 protobuf diff --git a/examples/dbrx/requirements.txt b/examples/dbrx/requirements.txt index 5de3a3747..44e7c6b65 100644 --- a/examples/dbrx/requirements.txt +++ b/examples/dbrx/requirements.txt @@ -1,5 +1,5 @@ --extra-index-url https://pypi.nvidia.com -tensorrt_llm==0.11.0.dev2024062500 +tensorrt_llm==0.12.0.dev2024070200 datasets~=2.14.5 evaluate~=0.4.1 rouge_score~=0.1.2 diff --git a/examples/enc_dec/README.md b/examples/enc_dec/README.md index 51ec74ef2..860642ddf 100644 --- a/examples/enc_dec/README.md +++ b/examples/enc_dec/README.md @@ -14,6 +14,7 @@ This document shows how to build and run an Encoder-Decoder (Enc-Dec) model in T - [Build TensorRT engine(s)](#build-tensorrt-engines) - [Run](#run) - [Run C++ runtime](#run-c-runtime) + - [Run with Triton Backend](#run-with-triton-backend) - [Run Python runtime](#run-python-runtime) - [Benchmark](#benchmark) - [Run BART with LoRA](#run-bart-with-lora) @@ -115,7 +116,7 @@ trtllm-build --checkpoint_dir tmp/trt_models/${MODEL_NAME}/${INFERENCE_PRECISION --use_custom_all_reduce disable \ --max_beam_width ${MAX_BEAM_WIDTH} \ --max_batch_size 8 \ - --max_seq_len 1224 \ + --max_input_len 1024 \ --gemm_plugin ${INFERENCE_PRECISION} \ --bert_attention_plugin ${INFERENCE_PRECISION} \ --gpt_attention_plugin ${INFERENCE_PRECISION} \ @@ -130,13 +131,14 @@ trtllm-build --checkpoint_dir tmp/trt_models/${MODEL_NAME}/${INFERENCE_PRECISION --use_custom_all_reduce disable \ --max_beam_width ${MAX_BEAM_WIDTH} \ --max_batch_size 8 \ + --max_input_len 1 \ --max_seq_len 201 \ + --max_encoder_input_len 1024 \ --gemm_plugin ${INFERENCE_PRECISION} \ --bert_attention_plugin ${INFERENCE_PRECISION} \ --gpt_attention_plugin ${INFERENCE_PRECISION} \ --remove_input_padding enable \ - --context_fmha disable \ - --max_input_len 1 + --context_fmha disable ``` @@ -167,7 +169,7 @@ trtllm-build --checkpoint_dir tmp/trt_models/${MODEL_NAME}/${INFERENCE_PRECISION --use_custom_all_reduce disable \ --max_beam_width ${MAX_BEAM_WIDTH} \ --max_batch_size 8 \ - --max_seq_len 1224 \ + --max_input_len 1024 \ --gemm_plugin ${INFERENCE_PRECISION} \ --bert_attention_plugin ${INFERENCE_PRECISION} \ --gpt_attention_plugin ${INFERENCE_PRECISION} \ @@ -182,12 +184,13 @@ trtllm-build --checkpoint_dir tmp/trt_models/${MODEL_NAME}/${INFERENCE_PRECISION --use_custom_all_reduce disable \ --max_beam_width ${MAX_BEAM_WIDTH} \ --max_batch_size 8 \ + --max_input_len 1 \ --max_seq_len 201 \ + --max_encoder_input_len 1024 \ --gemm_plugin ${INFERENCE_PRECISION} \ --bert_attention_plugin ${INFERENCE_PRECISION} \ --gpt_attention_plugin ${INFERENCE_PRECISION} \ - --remove_input_padding enable \ - --max_input_len 1 + --remove_input_padding enable # --context_fmha disable should be removed ``` @@ -211,11 +214,14 @@ For good usability, Python binding of the C++ runtime is provided. You can use t ```python # Inferencing via python binding of C++ runtime with inflight batching (IFB) -python3 ../run.py --engine_dir tmp/trt_engines/${MODEL_NAME}/${INFERENCE_PRECISION} --tokenizer_dir tmp/hf_models/${MODEL_NAME} --max_output_len 64 --input_text "translate English to German: The house is wonderful." +python3 ../run.py --engine_dir tmp/trt_engines/${MODEL_NAME}/${INFERENCE_PRECISION} --tokenizer_dir tmp/hf_models/${MODEL_NAME} --max_output_len 64 --num_beams=1 --input_text "translate English to German: The house is wonderful." ``` For pure C++ runtime, there is no example given yet. Please check the [`Executor`](../../cpp/include/tensorrt_llm/executor/executor.h) API to implement your own end-to-end workflow. It is highly recommended to leverage more encapsulated solutions such as the above C++ Python binding or [Triton backend](https://github.com/triton-inference-server/tensorrtllm_backend). +#### Run with Triton Backend +[Triton backend](https://github.com/triton-inference-server/tensorrtllm_backend/blob/main/docs/encoder_decoder.md) contains the tutorial on how to run encoder-decoder engines with Tritonserver. + #### Run Python runtime For pure Python runtime, you can still use the encoder-decoder specific script under `examples/enc_dec/`. @@ -287,7 +293,7 @@ trtllm-build --checkpoint_dir tmp/trt_models/bart-large-cnn/${INFERENCE_PRECISIO --use_custom_all_reduce disable \ --max_beam_width 1 \ --max_batch_size 8 \ - --max_seq_len 1224 \ + --max_input_len 1024 \ --gemm_plugin ${INFERENCE_PRECISION} \ --bert_attention_plugin ${INFERENCE_PRECISION} \ --gpt_attention_plugin ${INFERENCE_PRECISION} \ @@ -303,12 +309,13 @@ trtllm-build --checkpoint_dir tmp/trt_models/bart-large-cnn/${INFERENCE_PRECISIO --use_custom_all_reduce disable \ --max_beam_width 1 \ --max_batch_size 8 \ + --max_input_len 1 \ --max_seq_len 201 \ + --max_encoder_input_len 1024 \ --gemm_plugin ${INFERENCE_PRECISION} \ --bert_attention_plugin ${INFERENCE_PRECISION} \ --gpt_attention_plugin ${INFERENCE_PRECISION} \ --remove_input_padding disable \ - --max_input_len 1 \ --lora_plugin ${INFERENCE_PRECISION} \ --lora_dir tmp/hf_models/bart-large-cnn-samsum-lora/ \ --lora_target_modules attn_q cross_attn_q attn_v cross_attn_v @@ -342,8 +349,7 @@ python run.py \ ### Reminders -- Flan-T5 models have known issues regarding FP16 precision and using BF16 precision is recommended, regardless of TRT-LLM. While we are working on improving FP16 results, please stay with FP32 or BF16 precision for Flan-T5 family. -- Batched/Ragged input with beam search is having subtle issues with some sequence results being truncated. For the time being, please follow (1) if batch size = 1, no problem (2) if batched input is padded (i.e., not using `--remove_input_padding` flag), no problem (3) if batched input is ragged (i.e., using `--remove_input_padding`), only use greedy search for now. +- Flan-T5 models have known issues regarding FP16 precision and using BF16 precision is recommended, regardless of TRT-LLM. Please stay with FP32 or BF16 precision for Flan-T5 family. - For T5 and Flan-T5 family that have relative attention bias design, the relative attention table is split along `num_heads` dimension in Tensor Parallelism mode. Therefore, `num_heads` must be divisible by `tp_size`. Please be aware of this when setting the TP parameter. - For mBART, models that can control output languages (e.g. [`mbart-large-50-many-to-many-mmt`](https://huggingface.co/facebook/mbart-large-50-many-to-many-mmt)) are not currently supported, as the script does not support `ForcedBOSTokenLogitsProcessor` to control output languages. @@ -394,7 +400,7 @@ trtllm-build --checkpoint_dir tmp/trt_models/wmt14/${INFERENCE_PRECISION}/encode --use_custom_all_reduce disable \ --max_beam_width 1 \ --max_batch_size 8 \ - --max_seq_len 1224 \ + --max_input_len 1024 \ --bert_attention_plugin ${INFERENCE_PRECISION} \ --gpt_attention_plugin ${INFERENCE_PRECISION} \ --remove_input_padding disable @@ -406,11 +412,12 @@ trtllm-build --checkpoint_dir tmp/trt_models/wmt14/${INFERENCE_PRECISION}/decode --use_custom_all_reduce disable \ --max_beam_width 1 \ --max_batch_size 8 \ + --max_input_len 1 \ --max_seq_len 201 \ + --max_encoder_input_len 1024 \ --bert_attention_plugin ${INFERENCE_PRECISION} \ --gpt_attention_plugin ${INFERENCE_PRECISION} \ - --remove_input_padding disable \ - --max_input_len 1 + --remove_input_padding disable # Run mpirun --allow-run-as-root -np ${WORLD_SIZE} python3 run.py --engine_dir tmp/trt_engines/wmt14/${INFERENCE_PRECISION} --engine_name wmt14 --model_name tmp/fairseq_models/wmt14/${INFERENCE_PRECISION} --max_new_token=24 --num_beams=1 ``` diff --git a/examples/enc_dec/convert_checkpoint.py b/examples/enc_dec/convert_checkpoint.py index 506829aae..54b567c73 100755 --- a/examples/enc_dec/convert_checkpoint.py +++ b/examples/enc_dec/convert_checkpoint.py @@ -11,7 +11,8 @@ import safetensors from helper import convert_weight_to_dtype, fuse_qkv_one_layer, reshape, split -from transformers import (AutoModelForSeq2SeqLM, MBartForConditionalGeneration, +from transformers import (AutoModelForSeq2SeqLM, Blip2ForConditionalGeneration, + MBartForConditionalGeneration, Pix2StructForConditionalGeneration, T5ForConditionalGeneration, VisionEncoderDecoderModel) @@ -133,6 +134,8 @@ def parse_t5_config_by_component(config, component, args): 'encoder', 'num_heads') component_config.encoder_head_size = config.getint( 'encoder', 'd_kv') + component_config.decoder_start_token_id = config.getint( + 'decoder', 'decoder_start_token_id') else: assert False, 'Unsupported component!' @@ -310,6 +313,9 @@ def get_attn_module_name(component, block, layer, attn_type): return weights +convert_blip2_weights_to_tllm_safetensors = convert_t5_weights_to_tllm_safetensors # func alias + + def parse_nmt_config(args, model): config = configparser.ConfigParser() fairseq_config = vars(model.cfg.model) # Namespace --> dict @@ -420,6 +426,8 @@ def parse_nmt_config_by_component(config, component, args): 'd_kv', fallback=component_config.encoder_hidden_size // component_config.encoder_num_heads) + component_config.decoder_start_token_id = config.getint( + 'decoder', 'decoder_start_token_id') return component_config @@ -719,6 +727,13 @@ def parse_bart_config_by_component(config, component, args): fallback=component_config.encoder_hidden_size // component_config.encoder_num_heads) + # nougat has decoder_start_token_id = None, special handling + decoder_start_token_id = config.get('decoder', + 'decoder_start_token_id') + component_config.decoder_start_token_id = int( + decoder_start_token_id + ) if decoder_start_token_id != "None" else None + return component_config encoder_config = None @@ -1008,6 +1023,8 @@ def parse_pix2struct_config_by_component(config, component, args): args.encoder_head_size = config.getint('decoder', 'd_kv') args.position_embedding_type = config.get( 'structure', 'position_embedding_type') + args.decoder_start_token_id = config.getint( + 'decoder', 'decoder_start_token_id') else: assert False, 'Unsupported component!' @@ -1180,6 +1197,9 @@ def get_model(args): elif args.model_type == "pix2struct": model = Pix2StructForConditionalGeneration.from_pretrained( args.model_dir) + elif args.model_type == "blip2": + model = Blip2ForConditionalGeneration.from_pretrained( + args.model_dir).language_model return model @@ -1200,8 +1220,9 @@ def convert_checkpoint(args): kv_cache_quant_algo = None quant_algo = None - encoder_config, decoder_config = globals( - )[f'parse_{args.model_type}_config'](args, model) + model_type = args.model_type if args.model_type != "blip2" else "t5" + encoder_config, decoder_config = globals()[f'parse_{model_type}_config']( + args, model) additional_settings = ["gated_act"] @@ -1308,6 +1329,7 @@ def convert_checkpoint(args): 'encoder_head_size': decoder_config.encoder_head_size, 'skip_cross_qkv': args.skip_cross_qkv, 'use_implicit_relative_attention': args.use_implicit_relative_attention, + 'decoder_start_token_id': decoder_config.decoder_start_token_id, } for additional_setting in additional_settings: if hasattr(decoder_config, additional_setting): @@ -1374,11 +1396,13 @@ def convert(worker_rank, world_size, args, model_config, convert_args, if __name__ == "__main__": parser = argparse.ArgumentParser( formatter_class=argparse.RawTextHelpFormatter) - parser.add_argument('--model_type', - type=str, - default='t5', - choices=['t5', 'nmt', 'bart', 'pix2struct'], - help='Model to be converted.') + parser.add_argument( + '--model_type', + type=str, + default='t5', + choices=['t5', 'nmt', 'bart', 'pix2struct', 'blip2'], + help= + 'Multimodal type when this script is used for multimodal conversion.') parser.add_argument('--world_size', type=int, default=1, diff --git a/examples/enc_dec/helper.py b/examples/enc_dec/helper.py index 2b4f44cb7..501f278e5 100755 --- a/examples/enc_dec/helper.py +++ b/examples/enc_dec/helper.py @@ -69,7 +69,7 @@ def fuse_qkv_one_layer(params, attn_module_name, trtllm_layer_name, tp_size, def get_qkv_module_name(model_type): - if model_type == "t5": + if model_type in ["t5", "blip2"]: q = "q" k = "k" v = "v" diff --git a/examples/falcon/requirements.txt b/examples/falcon/requirements.txt index 77d72b907..44c2c4d93 100644 --- a/examples/falcon/requirements.txt +++ b/examples/falcon/requirements.txt @@ -1,5 +1,5 @@ --extra-index-url https://pypi.nvidia.com -tensorrt_llm==0.11.0.dev2024062500 +tensorrt_llm==0.12.0.dev2024070200 transformers>=4.31.0 datasets~=2.14.5 evaluate~=0.4.1 diff --git a/examples/gemma/requirements.txt b/examples/gemma/requirements.txt index 296bc6255..888837259 100644 --- a/examples/gemma/requirements.txt +++ b/examples/gemma/requirements.txt @@ -3,7 +3,7 @@ # WAR the new posting of "nvidia-cudnn-cu12~=9.0". # "jax[cuda12_pip]~=0.4.19" specifies "nvidia-cudnn-cu12>=8.9" but actually requires "nvidia-cudnn-cu12~=8.9". nvidia-cudnn-cu12~=8.9; platform_machine == "x86_64" -tensorrt_llm==0.11.0.dev2024062500 +tensorrt_llm==0.12.0.dev2024070200 flax~=0.8.0 # jax[cuda12_pip]~=0.4.19; platform_system != "Windows" jax~=0.4.19; platform_system == "Windows" @@ -13,3 +13,4 @@ h5py~=3.10.0 easydict~=1.11 rouge_score nltk +datasets==2.14.6 diff --git a/examples/gpt/README.md b/examples/gpt/README.md index 80c38314e..a750a2827 100644 --- a/examples/gpt/README.md +++ b/examples/gpt/README.md @@ -30,6 +30,7 @@ This document explains how to build the [GPT](https://huggingface.co/gpt2) model - [3. Embedding sharing](#3-embedding-sharing) - [GPT Variant - SantaCoder](#gpt-variant---santacoder) - [GPT Variant - StarCoder (v1 and v2)](#gpt-variant---starcoder-v1-and-v2) + - [Run StarCoder2 with LoRA](#run-starcoder2-with-lora) - [GPT-Next](#gpt-next) - [Prompt-tuning](#prompt-tuning) - [MultiLoRA with the Nemo checkpoint](#multilora-with-the-nemo-checkpoint) @@ -613,6 +614,44 @@ For StarCoder2, you can use almost the same steps as shown above. - Add `--max_attention_window_size 4096` when running with run.py or summarization, which enables the sliding window attention. - the sliding window size comes from the hf model [config.json](https://huggingface.co/bigcode/starcoder2-15b/blob/main/config.json#L23). +### Run StarCoder2 with LoRA + +TensorRT-LLM supports running StarCoder2 models with FP16/BF16/FP32 LoRA. In this section, we use starcoder2-15b as an example to show how to run an FP8 base model with FP16 LoRA module. + +* download the base model and lora model from HF + +```bash +git-lfs clone https://huggingface.co/bigcode/starcoder2-15b +git-lfs clone https://huggingface.co/KaQyn/peft-lora-starcoder2-15b-unity-copilot +``` + +* Quantize the StarCoder2 model to fp8 from HF +```bash +BASE_STARCODER2_MODEL=./starcoder2-15b +python ../quantization/quantize.py --model_dir ${BASE_STARCODER2_MODEL} \ + --dtype float16 \ + --qformat fp8 \ + --kv_cache_dtype fp8 \ + --output_dir starcoder2-15b/trt_ckpt/fp8/1-gpu \ + --calib_size 512 +``` + +* Build engine and run inference. +``` +trtllm-build --checkpoint_dir starcoder2-15b/trt_ckpt/fp8/1-gpu \ + --output_dir starcoder2-15b/trt_engines/fp8_lora/1-gpu \ + --gemm_plugin auto \ + --lora_plugin auto \ + --lora_dir ./peft-lora-starcoder2-15b-unity-copilot + +python ../run.py --engine_dir starcoder2-15b/trt_engines/fp8_lora/1-gpu \ + --max_output_len 20 \ + --tokenizer_dir ${BASE_STARCODER2_MODEL} \ + --input_text "def print_hello_world():" \ + --lora_task_uids 0 \ + --no_add_special_tokens \ + --use_py_session +``` ## GPT-Next diff --git a/examples/gpt/requirements.txt b/examples/gpt/requirements.txt index 03735ad42..9c1091653 100644 --- a/examples/gpt/requirements.txt +++ b/examples/gpt/requirements.txt @@ -1,5 +1,5 @@ --extra-index-url https://pypi.nvidia.com -tensorrt_llm==0.11.0.dev2024062500 +tensorrt_llm==0.12.0.dev2024070200 datasets~=2.14.5 evaluate~=0.4.1 rouge_score~=0.1.2 diff --git a/examples/gptj/requirements.txt b/examples/gptj/requirements.txt index 9cd6ac378..7800b0ea6 100644 --- a/examples/gptj/requirements.txt +++ b/examples/gptj/requirements.txt @@ -1,5 +1,5 @@ --extra-index-url https://pypi.nvidia.com -tensorrt_llm==0.11.0.dev2024062500 +tensorrt_llm==0.12.0.dev2024070200 datasets~=2.14.5 evaluate~=0.4.1 rouge_score~=0.1.2 diff --git a/examples/gptneox/requirements.txt b/examples/gptneox/requirements.txt index b6d9a8b15..aa242a331 100644 --- a/examples/gptneox/requirements.txt +++ b/examples/gptneox/requirements.txt @@ -1,5 +1,5 @@ --extra-index-url https://pypi.nvidia.com -tensorrt_llm==0.11.0.dev2024062500 +tensorrt_llm==0.12.0.dev2024070200 datasets~=2.14.5 rouge_score~=0.1.2 evaluate~=0.4.1 diff --git a/examples/grok/requirements.txt b/examples/grok/requirements.txt index 7480cc654..9f62ca868 100644 --- a/examples/grok/requirements.txt +++ b/examples/grok/requirements.txt @@ -1,6 +1,6 @@ -f https://storage.googleapis.com/jax-releases/jax_cuda_releases.html --extra-index-url https://pypi.nvidia.com -tensorrt_llm==0.11.0.dev2024062500 +tensorrt_llm==0.12.0.dev2024070200 datasets==2.14.6 evaluate~=0.4.1 rouge_score~=0.1.2 diff --git a/examples/hf_lora_convert.py b/examples/hf_lora_convert.py index 525be45fd..4808b303f 100644 --- a/examples/hf_lora_convert.py +++ b/examples/hf_lora_convert.py @@ -59,14 +59,34 @@ def get_all_lora_weights(lora_weights): return all_weights +def preprocess_lora_weights(lora_model): + # Swap weights of gate_up_proj + for key, value in lora_model.items(): + if "gate_up_proj.lora_B.weight" in key: + print("Swap {}".format(key)) + original_weights = value.contiguous().clone() + half_split = original_weights.shape[0] // 2 + first_half = original_weights[:half_split, :] + second_half = original_weights[half_split:, :] + value = torch.cat((second_half, first_half), dim=0) + lora_model[key] = value + return lora_model + + hf_modules_to_trtllm_modules = { "q_proj": "attn_q", "v_proj": "attn_v", "k_proj": "attn_k", + "qkv_proj": "attn_qkv", + "query_key_value": "attn_qkv", "o_proj": "attn_dense", + "dense": "attn_dense", "gate_proj": "mlp_h_to_4h", "down_proj": "mlp_4h_to_h", - "up_proj": "mlp_gate" + "up_proj": "mlp_gate", + "gate_up_proj": "mlp_h_to_4h", + "c_fc": "mlp_h_to_4h", + "c_proj": "mlp_4h_to_h", } # lora modules on llama hf_modules_to_module_id = { k: LoraManager.LORA_MODULE_IDS[v] @@ -89,6 +109,7 @@ def convert_hf_model(model_dir, dtype, out_dir): scale = alpha / rank lora_model = load_state_dict(get_model_path(model_dir, "adapter_model")) + lora_model = preprocess_lora_weights(lora_model) all_weights = get_all_lora_weights(lora_model) converted_weights = [] converted_config = [] diff --git a/examples/high-level-api/README.md b/examples/high-level-api/README.md index 7896488f3..6e46bb669 100644 --- a/examples/high-level-api/README.md +++ b/examples/high-level-api/README.md @@ -40,7 +40,7 @@ The HLAPI supports three kinds of model formats: 2. TensorRT-LLM engine built by trtllm-build tool or saved by the HLAPI 3. TensorRT-LLM checkpoints, converted by `convert_checkpoint.py` in examples -All kinds of models could be used directly by the HLAPI, and the `ModelConfig()` could accept any kind of them. +All kinds of models could be used directly by the HLAPI, and the `LLM(model=)` could accept any kind of them. Let's elaborate on the preparation of the three kinds of model formats. @@ -72,8 +72,7 @@ There are two ways to build the TensorRT-LLM engine: 2. Use the HLAPI to save one: ```python -config = ModelConfig() -llm = LLM(config) +llm = LLM() # Save engine to local disk llm.save() @@ -86,14 +85,13 @@ For step-by-step guidance on checkpoint conversion, please refer to the LLaMA's ## Basic usage -To use the API, import the `LLM` and `ModelConfig` from the `tensorrt_llm` package and create an LLM object with a HuggingFace model directly. +To use the API, import the `LLM` from the `tensorrt_llm` package and create an LLM object with a HuggingFace model directly. For example: ``` python -from tensorrt_llm import LLM, ModelConfig +from tensorrt_llm import LLM -config = ModelConfig(model_dir=) -llm = LLM(config) +llm = LLM(model=) ``` It will trigger TRT-LLM engine building in the backend, and create a HuggingFace tokenizer by default to support an end-to-end generation. @@ -109,7 +107,7 @@ for output in llm.generate(prompts): The output might be something like: ``` python -GenerationOutput(text="with a picture.\nI'm a writer, but I'm also a photographer.") +RequestOutput(request_id=2, prompt='To tell a story', prompt_token_ids=[1, 1763, 2649, 263, 5828], outputs=[CompletionOutput(index=0, text=', you need to have a beginning, a middle, and an end.\nThe beginning is the introduction of the characters and the setting.\nThe middle is', token_ids=[29892, 366, 817, 304, 505, 263, 6763, 29892, 263, 7256, 29892, 322, 385, 1095, 29889, 13, 1576, 6763, 338, 278, 18707, 310, 278, 4890, 322, 278, 4444, 29889, 13, 1576, 7256, 338], cumulative_logprob=None, logprobs=[])], finished=True) ``` You can also dump the runtime engine to disk, and load from the engine file directly in the next run to save the engine building time from the HuggingFace model. @@ -119,25 +117,22 @@ You can also dump the runtime engine to disk, and load from the engine file dire llm.save() # next time -config = ModelConfig(model_dir=) -llm = LLM(config) +llm = LLM(model=) ``` In other words, the `model_dir` could accept either a HugggingFace model, a built TensorRT-LLM engine, or a TensorRT-LLM checkpoint, and the `LLM()` will do the rest work silently for end-to-end execution. ## Quantization -By simply setting several flags in the ModelConfig, TensorRT-LLM can quantize the HuggingFace model automatically. For example, to perform an Int4 AWQ quantization, the following code will trigger the model quantization. +By simply setting several flags in the `LLM`, TensorRT-LLM can quantize the HuggingFace model automatically. For example, to perform an Int4 AWQ quantization, the following code will trigger the model quantization. ``` python -from tensorrt_llm.quantization import QuantAlgo +from tensorrt_llm.hlapi import QuantConfig, QuantAlgo -config = ModelConfig(model_dir=) +quant_config = QuantConfig(quant_algo=QuantAlgo.W4A16_AWQ) -config.quant_config.quant_algo = QuantAlgo.W4A16_AWQ - -llm = LLM(config) +llm = LLM(, quant_config=quant_config) ``` ## Parallelism @@ -146,10 +141,9 @@ llm = LLM(config) It is easy to enable Tensor Parallelism in the HLAPI. For example, setting `parallel_config.tp_size=2` to perform a 2-way parallelism: ```python -from tensorrt_llm import LLM, ModelConfig +from tensorrt_llm.hlapi import LLM -config = ModelConfig(model_dir=) -config.parallel_config.tp_size = 2 +llm = LLM(, tensor_parallel_size=2) ``` ### Pipeline Parallelism @@ -163,14 +157,12 @@ config.parallel_config.pp_size = 4 ### Automatic Parallelism (in preview) -By simply enabling `parallel_config.auto_parallel` in the ModelConfig, TensorRT-LLM can parallelize the model automatically. For example, setting `parallel_config.world_size` to perform a 2-way parallelism: +By simply enabling `auto_parallel` in the `LLM` class, TensorRT-LLM can parallelize the model automatically. For example, setting `world_size` to perform a 2-way parallelism: ``` python -from tensorrt_llm import LLM, ModelConfig +from tensorrt_llm import LLM -config = ModelConfig(model_dir=) -config.parallel_config.auto_parallel = True -config.parallel_config.world_size = 2 +llm = LLM(, auto_parallel=True, world_size=2) ``` ### Multi-GPU multi-node (MGMN) support @@ -183,12 +175,8 @@ Firstly, it is suggested to build the engine offline with the `trtllm-build` too Secondly, you need to prepare a Python file containing the HLAPI task, a naive example is as below, note that, this Python script will be executed only once on MPI rank0, and it looks nothing special compared to the single-node-multi-gpu scenario, such as TP or PP. ```python -config = ModelConfig(model_dir=) -# Set the parallel_config to the number of GPUs you want to use -config.parallel_config.tp_size = 4 -config.parallel_config.pp_size = 2 - -llm = LLM(config) +# Set the tensor_parallel_size and pipeline_parallel_size to the number of GPUs you want to use +llm = LLM(model=, tensor_parallel_size=4, pipeline_parallel_size=2) for output in llm.generate([[32, 12]]): print(output) ``` @@ -218,9 +206,7 @@ Considering the Slurm or other cluster management systems may be highly customiz With the high-level API, you can also perform asynchronous generation with the `generate_async` method. For example: ```python -config = ModelConfig(model_dir=) - -llm = LLM(config, async_mode=True) +llm = LLM(model=) async for output in llm.generate_async(, streaming=True): print(output) @@ -258,12 +244,12 @@ With SamplingParams, you can customize the sampling strategy, such as beam searc To enable beam search with a beam size of 4, set the `sampling_params` as follows: ```python -from tensorrt_llm import ModelConfig, LLM -from tensorrt_llm.hlapi import SamplingParams +from tensorrt_llm.hlapi import LLM, SamplingParams, BuildConfig -config = ModelConfig(model_dir=, max_beam_width=4) +build_config = BuildConfig() +build_config.max_beam_width = 4 -llm = LLM(config) +llm = LLM(, build_config=build_config) # Let the LLM object generate text with the default sampling strategy, or # you can create a SamplingParams object as well with several fields set manually sampling_params = SamplingParams(beam_width=4) # current limitation: beam_width should be equal to max_beam_width @@ -282,18 +268,15 @@ Please refer to these classes for more details. ### Runtime customization -For `kv_cache_config`, `capacity_scheduling_policy` and `streaming_llm` features, please refer to LLaMA's [README](../llama/README.md) for more details, the high-level API supports these features as well by setting the corresponding fields in the `LLM()` constructor. +For `kv_cache_config` and `streaming_llm` features, please refer to LLaMA's [README](../llama/README.md) for more details, the high-level API supports these features as well by setting the corresponding fields in the `LLM()` constructor. ```python -from tensorrt_llm import ModelConfig, LLM -from tensorrt_llm.hlapi import KvCacheConfig, CapacitySchedulerPolicy +from tensorrt_llm.hlapi import LLM, KvCacheConfig -config = ModelConfig(model_dir=) -llm = LLM(config, +llm = LLM(, kv_cache_config=KvCacheConfig( max_new_tokens=128, - free_gpu_memory_fraction=0.8), - capacity_scheduling_policy=CapacitySchedulerPolicy.GUARANTEED_NO_EVICT) + free_gpu_memory_fraction=0.8)) ``` ### Tokenizer customization @@ -301,7 +284,7 @@ llm = LLM(config, By default, the high-level API uses transformers’ `AutoTokenizer`. You can override it with your own tokenizer by passing it when creating the LLM object. For example: ```python -llm = LLM(config, tokenizer=) +llm = LLM(, tokenizer=) ``` The LLM() workflow should use your tokenizer instead. @@ -309,20 +292,24 @@ The LLM() workflow should use your tokenizer instead. It is also possible to input token IDs directly without Tokenizers with the following code, note that the result will be also IDs without text since the tokenizer is not used. ``` python -llm = LLM(config) +llm = LLM() -for output in llm.generate([32, 12]): ... +for output in llm.generate([32, 12]): + ... ``` ### Disabling tokenizer -For performance considerations, you can disable the tokenizer by passing the token ID list to the `LLM.generate/_async` method. For example: +For performance considerations, you can disable the tokenizer by passing `skip_tokenizer_init=True` when creating `LLM`. In this case, `LLM.generate` and `LLM.generate_async` will expect prompt token ids as input. For example: ```python -config = ModelConfig(model_dir=) - -llm = LLM(config) +llm = LLM() for output in llm.generate([[32, 12]]): print(output) ``` -You will get something like `GenerationResult(text='', token_ids=[23, 14, 3, 29871, 3], ...)`, note that the `text` field is empty since the tokenizer is not activated. +You will get something like: +```python +RequestOutput(request_id=1, prompt=None, prompt_token_ids=[1, 15043, 29892, 590, 1024, 338], outputs=[CompletionOutput(index=0, text='', token_ids=[518, 10858, 4408, 29962, 322, 306, 626, 263, 518, 10858, 20627, 29962, 472, 518, 10858, 6938, 1822, 306, 626, 5007, 304, 4653, 590, 4066, 297, 278, 518, 11947, 18527, 29962, 2602, 472], cumulative_logprob=None, logprobs=[])], finished=True) +``` + +Note that the `text` field in `CompletionOutput` is empty since the tokenizer is deactivated. diff --git a/examples/high-level-api/llm_examples.py b/examples/high-level-api/llm_examples.py index 7e51982ff..07f7f9824 100644 --- a/examples/high-level-api/llm_examples.py +++ b/examples/high-level-api/llm_examples.py @@ -6,10 +6,11 @@ import click import torch -from tensorrt_llm import LLM, ModelConfig -from tensorrt_llm.hlapi.llm import KvCacheConfig, SamplingParams +from tensorrt_llm import LLM +from tensorrt_llm.hlapi import KvCacheConfig +from tensorrt_llm.hlapi.llm import SamplingParams +from tensorrt_llm.hlapi.llm_utils import KvCacheConfig, QuantAlgo, QuantConfig from tensorrt_llm.hlapi.utils import get_device_count -from tensorrt_llm.quantization import QuantAlgo # NOTE, Currently, the following examples are only available for LLaMA models. @@ -62,20 +63,19 @@ def run_llm_generate( pp_size: The number of GPUs for Pipeline Parallel. ''' - config = ModelConfig(model_dir) # Avoid the tp_size and pp_size setting override the ones loaded from built engine - if tp_size > 1: config.parallel_config.tp_size = tp_size - if pp_size > 1: config.parallel_config.pp_size = pp_size - - if get_device_count() < config.parallel_config.world_size: + world_size = tp_size * pp_size + if get_device_count() < world_size: print( "Skip the example for TP!!! Since the number of GPUs is less than required" ) return - if config.parallel_config.world_size > 1: + if world_size > 1: print(f'Running LLM with Tensor Parallel on {tp_size} GPUs.') - llm = LLM(config) + llm = LLM(model_dir, + tensor_parallel_size=tp_size, + pipeline_parallel_size=pp_size) if engine_dir and os.path.abspath(model_dir) != os.path.abspath(engine_dir): print(f"Saving engine to {engine_dir}...") @@ -119,19 +119,17 @@ def run_llm_generate_async_example(prompt: str, if tp_size > 1: print(f'Running LLM with Tensor Parallel on {tp_size} GPUs.') - config = ModelConfig(model_dir) # Avoid the tp_size and pp_size setting override the ones loaded from built engine - if tp_size > 1: config.parallel_config.tp_size = tp_size - if pp_size > 1: config.parallel_config.pp_size = pp_size - - llm = LLM(config, + llm = LLM(model_dir, + tensor_parallel_size=tp_size, + pipeline_parallel_size=pp_size, kv_cache_config=KvCacheConfig(free_gpu_memory_fraction=0.4)) prompts = parse_prompts(prompt, False) async def task(prompt: str): outputs = [] async for output in llm.generate_async(prompt, streaming=streaming): - outputs.append(output.text) + outputs.append(output.outputs[0].text) print(' '.join(outputs)) async def main(): @@ -163,14 +161,14 @@ def run_llm_with_quantization(prompt: str, model_dir: str, quant_type: str): print("Hopper GPUs are required for fp8 quantization") return - config = ModelConfig(model_dir) + quant_config = QuantConfig() if quant_type == 'int4_awq': - config.quant_config.quant_algo = QuantAlgo.W4A16_AWQ + quant_config.quant_algo = QuantAlgo.W4A16_AWQ else: - config.quant_config.quant_algo = QuantAlgo.FP8 - config.quant_config.kv_cache_quant_algo = QuantAlgo.FP8 + quant_config.quant_algo = QuantAlgo.FP8 + quant_config.kv_cache_quant_algo = QuantAlgo.FP8 - llm = LLM(config) + llm = LLM(model_dir, quant_config=quant_config) prompts = parse_prompts(prompt, False) for output in llm.generate(prompts): @@ -181,22 +179,22 @@ def run_llm_with_quantization(prompt: str, model_dir: str, quant_type: str): @click.option('--prompt', type=str, default="What is LLM?") @click.option('--model_dir', type=str, help='The directory of the model.') def run_llm_with_async_future(prompt: str, model_dir: str): - config = ModelConfig(model_dir) - llm = LLM(config, + llm = LLM(model_dir, kv_cache_config=KvCacheConfig(free_gpu_memory_fraction=0.4)) prompts = parse_prompts(prompt) # The result of generate() is similar to a Future, it won't block the main thread, call .result() to explicitly wait for the result - for generation in llm.generate_async(prompts): + futures = [llm.generate_async(prompt) for prompt in prompts] + for future in futures: # .result() is a blocking call, call it when you want to wait for the result - output = generation.result() - print(output.text) + output = future.result() + print(output.outputs[0].text) # Similar to .result(), there is an async version of .result(), which is .aresult(), and it works with the generate_async(). async def task(prompt: str): generation = llm.generate_async(prompt, streaming=False) output = await generation.aresult() - print(output.text) + print(output.outputs[0].text) async def main(): tasks = [task(prompt) for prompt in prompts] @@ -224,11 +222,11 @@ def run_llm_with_auto_parallel(prompt: str, if world_size > 1: print(f'Running LLM with Auto Parallel on {world_size} GPUs.') - config = ModelConfig(model_dir) - config.parallel_config.auto_parallel = True - config.parallel_config.world_size = world_size - - llm = LLM(config) + llm = LLM( + model_dir, + auto_parallel=True, + world_size=world_size, + ) prompts = parse_prompts(prompt) for output in llm.generate(prompts): diff --git a/examples/high-level-api/quickstart_example.py b/examples/high-level-api/quickstart_example.py new file mode 100644 index 000000000..ccf77ed20 --- /dev/null +++ b/examples/high-level-api/quickstart_example.py @@ -0,0 +1,19 @@ +from tensorrt_llm import LLM, SamplingParams + +prompts = [ + "Hello, my name is", + "The president of the United States is", + "The capital of France is", + "The future of AI is", +] +sampling_params = SamplingParams(temperature=0.8, top_p=0.95) + +llm = LLM(model="TinyLlama/TinyLlama-1.1B-Chat-v1.0") + +outputs = llm.generate(prompts, sampling_params) + +# Print the outputs. +for output in outputs: + prompt = output.prompt + generated_text = output.outputs[0].text + print(f"Prompt: {prompt!r}, Generated text: {generated_text!r}") diff --git a/examples/high-level-api/requirements.txt b/examples/high-level-api/requirements.txt index 7833f8eee..476362335 100644 --- a/examples/high-level-api/requirements.txt +++ b/examples/high-level-api/requirements.txt @@ -1,2 +1,2 @@ --extra-index-url https://pypi.nvidia.com -tensorrt_llm==0.11.0.dev2024062500 +tensorrt_llm==0.12.0.dev2024070200 diff --git a/examples/internlm/requirements.txt b/examples/internlm/requirements.txt index e86e7fd2d..2dd11374a 100644 --- a/examples/internlm/requirements.txt +++ b/examples/internlm/requirements.txt @@ -1,5 +1,5 @@ --extra-index-url https://pypi.nvidia.com -tensorrt_llm==0.11.0.dev2024062500 +tensorrt_llm==0.12.0.dev2024070200 datasets==2.14.5 rouge_score~=0.1.2 sentencepiece~=0.1.99 diff --git a/examples/llama/README.md b/examples/llama/README.md index 84ebc94c1..97cb61177 100644 --- a/examples/llama/README.md +++ b/examples/llama/README.md @@ -888,14 +888,14 @@ mpirun -n 8 --allow-run-as-root \ ## Run models with LoRA -* download the base model and lora model from HF +Download the base model and lora model from HF: ```bash git-lfs clone https://huggingface.co/meta-llama/Llama-2-13b-hf git-lfs clone https://huggingface.co/hfl/chinese-llama-2-lora-13b ``` -* Build engine, setting `--lora_plugin` and `--lora_dir`. If lora has separate lm_head and embedding, they will replace lm_head and embedding of base model. +Build engine, setting `--lora_plugin` and `--lora_dir`. If lora has separate lm_head and embedding, they will replace lm_head and embedding of base model. ```bash python convert_checkpoint.py --model_dir Llama-2-13b-hf \ @@ -913,7 +913,7 @@ trtllm-build --checkpoint_dir ./tllm_checkpoint_2gpu \ --lora_dir chinese-llama-2-lora-13b ``` -* Run inference. Need to setup the `lora_dir`. Remember to use lora tokenizer because lora model has larger vocab size. +Run inference. Remember to use lora tokenizer because lora model has larger vocab size. ```bash mpirun -n 2 python ../run.py --engine_dir "/tmp/new_lora_13b/trt_engines/fp16/2-gpu/" \ @@ -927,6 +927,7 @@ mpirun -n 2 python ../run.py --engine_dir "/tmp/new_lora_13b/trt_engines/fp16/2- Input: "今天天气很好,我到公园的时候," Output: "发现公园里到处都是人,有的在跑步,有的在打羽毛球,还有的在跳绳,我和妈妈一起在公园里散步,我和妈妈在公园里散步的时候,看见了一位老爷爷在打羽毛球" ``` + Users who want to skip LoRA module may pass uid -1 with `--lora_task_uids -1`. In that case, the model will not run the LoRA module and the results will be different. Since the LoRA tokenizer, embedding and LM head are still used, diff --git a/examples/llama/requirements.txt b/examples/llama/requirements.txt index 814a8c824..f9f50349b 100644 --- a/examples/llama/requirements.txt +++ b/examples/llama/requirements.txt @@ -1,5 +1,5 @@ --extra-index-url https://pypi.nvidia.com -tensorrt_llm==0.11.0.dev2024062500 +tensorrt_llm==0.12.0.dev2024070200 datasets==2.14.6 evaluate~=0.4.1 rouge_score~=0.1.2 diff --git a/examples/mamba/requirements.txt b/examples/mamba/requirements.txt index c64cfbf6a..4ffa84ff5 100644 --- a/examples/mamba/requirements.txt +++ b/examples/mamba/requirements.txt @@ -1,5 +1,5 @@ --extra-index-url https://pypi.nvidia.com -tensorrt_llm==0.11.0.dev2024062500 +tensorrt_llm==0.12.0.dev2024070200 transformers>=4.39.0 datasets~=2.14.5 evaluate diff --git a/examples/medusa/requirements.txt b/examples/medusa/requirements.txt index f396bbdcc..21925c333 100644 --- a/examples/medusa/requirements.txt +++ b/examples/medusa/requirements.txt @@ -1,5 +1,5 @@ --extra-index-url https://pypi.nvidia.com -tensorrt_llm==0.11.0.dev2024062500 +tensorrt_llm==0.12.0.dev2024070200 datasets~=2.14.5 rouge_score~=0.1.2 sentencepiece~=0.1.99 diff --git a/examples/mixtral/requirements.txt b/examples/mixtral/requirements.txt index 6a15339a6..bdf3bc52d 100644 --- a/examples/mixtral/requirements.txt +++ b/examples/mixtral/requirements.txt @@ -1,4 +1,4 @@ --extra-index-url https://pypi.nvidia.com -tensorrt_llm==0.11.0.dev2024062500 +tensorrt_llm==0.12.0.dev2024070200 transformers==4.38.2 accelerate==0.25.0 diff --git a/examples/mmlu.py b/examples/mmlu.py index 0ce98a7be..c32afface 100644 --- a/examples/mmlu.py +++ b/examples/mmlu.py @@ -194,6 +194,7 @@ def gen_prompt(train_df, subject, k=-1): def evaluate(args, subject, pipeline, dev_df, test_df): + rank = tensorrt_llm.mpi_rank() cors = [] all_probs = [] for i in range(test_df.shape[0]): @@ -213,18 +214,22 @@ def evaluate(args, subject, pipeline, dev_df, test_df): label = test_df.iloc[i, test_df.shape[1] - 1] pred = pipeline(prompt) - probs = [0 for _ in get_choices()] - cor = pred.strip().startswith(label) - cors.append(cor) - all_probs.append(probs) + if rank == 0: + probs = [0 for _ in get_choices()] + cor = pred.strip().startswith(label) + cors.append(cor) + all_probs.append(probs) - acc = np.mean(cors) - cors = np.array(cors) + if rank == 0: + acc = np.mean(cors) + cors = np.array(cors) - all_probs = np.array(all_probs) - print("Average accuracy {:.3f} - {}".format(acc, subject)) + all_probs = np.array(all_probs) + print("Average accuracy {:.3f} - {}".format(acc, subject)) - return cors, acc, all_probs + return cors, acc, all_probs + else: + return None, 0, None def get_tokenizer(ckpt_path, max_seq_len): @@ -252,6 +257,7 @@ def __init__(self, tokenizer, model, model_name, pad_id, end_id, self.max_attention_window_size = max_attention_window_size def __call__(self, prompt): + rank = tensorrt_llm.mpi_rank() # Run the model in batch size 1 and beam size 1 inputs = self.tokenizer.encode(prompt, return_tensors="pt").squeeze(0) batch_input_ids = [inputs] @@ -296,9 +302,13 @@ def __call__(self, prompt): top_p=top_p, ) torch.cuda.synchronize() - output_ids = outputs[0, 0, input_lengths[0]:] + if rank == 0: + output_ids = outputs[0, 0, input_lengths[0]:] - return self.tokenizer.decode(output_ids, skip_special_tokens=True) + if rank == 0: + return self.tokenizer.decode(output_ids, skip_special_tokens=True) + else: + return None def check_valid_length(self, prompt): if isinstance(self.model, nn.Module): @@ -410,19 +420,20 @@ def main(): cat_cors[key].append(cors) all_cors.append(cors) - for subcat in subcat_cors: - subcat_acc = np.mean(np.concatenate(subcat_cors[subcat])) - print("Average accuracy {:.3f} - {}".format(subcat_acc, subcat)) + if runtime_rank == 0: + for subcat in subcat_cors: + subcat_acc = np.mean(np.concatenate(subcat_cors[subcat])) + print("Average accuracy {:.3f} - {}".format(subcat_acc, subcat)) - for cat in cat_cors: - cat_acc = np.mean(np.concatenate(cat_cors[cat])) - print("Average accuracy {:.3f} - {}".format(cat_acc, cat)) + for cat in cat_cors: + cat_acc = np.mean(np.concatenate(cat_cors[cat])) + print("Average accuracy {:.3f} - {}".format(cat_acc, cat)) - weighted_acc = np.mean(np.concatenate(all_cors)) - print("Average accuracy: {:.3f}".format(weighted_acc)) - if args.check_accuracy: - assert weighted_acc >= args.accuracy_threshold, f"Expected accuracy >= {args.accuracy_threshold} while got {weighted_acc}" - return weighted_acc + weighted_acc = np.mean(np.concatenate(all_cors)) + print("Average accuracy: {:.3f}".format(weighted_acc)) + if args.check_accuracy: + assert weighted_acc >= args.accuracy_threshold, f"Expected accuracy >= {args.accuracy_threshold} while got {weighted_acc}" + return weighted_acc if __name__ == "__main__": diff --git a/examples/model_api/README.md b/examples/model_api/README.md index cae86dfa3..937fef9bc 100644 --- a/examples/model_api/README.md +++ b/examples/model_api/README.md @@ -16,7 +16,7 @@ Note that multi GPU can also support the chat scenario, need to add additional c The example only targets to demonstrate the TRT-LLM API usage here, so it uses pre-defined dataset for simplicity. ```bash -python ./llama_multi_gpu.py --hf_model_dir --engine_dir ./llama.engine.tp2 -c --tp_size 2 +python ./llama_multi_gpu.py --hf_model_dir --engine_dir ./llama.engine.tp2 --tp_size 2 ``` # Quantization diff --git a/examples/model_api/llama.py b/examples/model_api/llama.py index 3d182a085..2a6a51e3a 100644 --- a/examples/model_api/llama.py +++ b/examples/model_api/llama.py @@ -2,6 +2,8 @@ import os from pathlib import Path +from transformers import AutoTokenizer + import tensorrt_llm from tensorrt_llm import BuildConfig, build from tensorrt_llm.executor import GenerationExecutor @@ -11,7 +13,7 @@ def read_input(): while (True): - input_text = input("<") + input_text = input("< ") if input_text in ("q", "quit"): break yield input_text @@ -51,7 +53,7 @@ def main(): max_batch_size=1) # just for fast build, not best for production build_config.builder_opt = 0 - build_config.plugin_config.gemm_plugin = "float16" + build_config.plugin_config.gemm_plugin = 'auto' if args.clean_build or not args.engine_dir.exists(): args.engine_dir.mkdir(exist_ok=True, parents=True) @@ -60,12 +62,14 @@ def main(): engine = build(llama, build_config) engine.save(args.engine_dir) - tokenizer_dir = args.hf_model_dir - executor = GenerationExecutor.create(args.engine_dir, tokenizer_dir) + tokenizer = AutoTokenizer.from_pretrained(args.hf_model_dir) + executor = GenerationExecutor.create(args.engine_dir) sampling_params = SamplingParams(max_new_tokens=20) for inp in read_input(): - output = executor.generate(inp, sampling_params=sampling_params) - print(f">{output.text}") + output = executor.generate(tokenizer.encode(inp), + sampling_params=sampling_params) + print(f"> {tokenizer.decode(output.outputs[0].token_ids)}") -main() +if __name__ == "__main__": + main() diff --git a/examples/model_api/llama_multi_gpu.py b/examples/model_api/llama_multi_gpu.py index eb066770d..abedb7d5b 100644 --- a/examples/model_api/llama_multi_gpu.py +++ b/examples/model_api/llama_multi_gpu.py @@ -1,13 +1,13 @@ import argparse import os -from pathlib import Path from cuda import cudart from mpi4py.futures import MPIPoolExecutor +from transformers import AutoTokenizer import tensorrt_llm from tensorrt_llm import BuildConfig, Mapping, build, mpi_barrier -from tensorrt_llm.executor import GenerationExecutorWorker, SamplingParams +from tensorrt_llm.executor import ExecutorBindingsWorker, SamplingParams from tensorrt_llm.models import LLaMAForCausalLM @@ -29,7 +29,7 @@ def build_and_run_llama(hf_model_dir, engine_dir, tp_size, rank): max_seq_len=512, max_batch_size=8) build_config.builder_opt = 0 # fast build for demo, pls avoid using this in production, since inference might be slower - build_config.plugin_config.gemm_plugin = 'float16' # for fast build, tune inference perf based on your needs + build_config.plugin_config.gemm_plugin = 'auto' # for fast build, tune inference perf based on your needs mapping = Mapping(world_size=tp_size, rank=rank, tp_size=tp_size) llama = LLaMAForCausalLM.from_hugging_face(hf_model_dir, mapping=mapping) engine = build(llama, build_config) @@ -37,19 +37,22 @@ def build_and_run_llama(hf_model_dir, engine_dir, tp_size, rank): mpi_barrier() # make sure every rank engine build finished ## Generation - tokenizer_dir = hf_model_dir - generate_len = 20 # change on your needs, hard code for simplicity here - executor = GenerationExecutorWorker(Path(engine_dir), tokenizer_dir) - - sampling_params = SamplingParams(max_new_tokens=generate_len) - - output_streams = executor.generate_async(dataset(), - True, - sampling_params=sampling_params) - if rank == 0: - for stream in output_streams: - for state in stream: - print(f"Output: {state.text}") + tokenizer = AutoTokenizer.from_pretrained(hf_model_dir) + sampling_params = SamplingParams(max_new_tokens=20) + + with ExecutorBindingsWorker(engine_dir) as executor: + executor.block_subordinates() + + for inp in dataset(): + stream_output = executor.generate_async( + tokenizer.encode(inp), + sampling_params=sampling_params, + streaming=True) + if rank == 0: + for state in stream_output: + print( + f"Output: {tokenizer.decode(state.outputs[0].token_ids)}" + ) mpi_barrier() return True @@ -76,22 +79,23 @@ def parse_args(): return parser.parse_args() -def run_llama(args): +def main(args): status, gpus = cudart.cudaGetDeviceCount() assert status == 0 and gpus >= args.tp_size, f"The test needs at least {args.tp_size} GPUs, skipping" if not os.path.exists(args.engine_dir): os.makedirs(args.engine_dir, exist_ok=True) + ## Build engine in parallel with MPIPoolExecutor(max_workers=args.tp_size) as pool: results = pool.map(build_and_run_llama, [args.hf_model_dir] * args.tp_size, [args.engine_dir] * args.tp_size, [args.tp_size] * args.tp_size, range(args.tp_size)) for r in results: - assert r == True + assert r is True if __name__ == "__main__": args = parse_args() - run_llama(args) + main(args) diff --git a/examples/model_api/llama_quantize.py b/examples/model_api/llama_quantize.py index 19fd48f58..52cc95c7e 100644 --- a/examples/model_api/llama_quantize.py +++ b/examples/model_api/llama_quantize.py @@ -2,6 +2,8 @@ import os from pathlib import Path +from transformers import AutoTokenizer + import tensorrt_llm from tensorrt_llm import BuildConfig, build from tensorrt_llm.executor import GenerationExecutor @@ -13,7 +15,7 @@ def read_input(): while (True): - input_text = input("<") + input_text = input("< ") if input_text in ("q", "quit"): break yield input_text @@ -47,7 +49,6 @@ def parse_args(): def main(): tensorrt_llm.logger.set_level('verbose') args = parse_args() - tokenizer_dir = args.hf_model_dir max_batch_size, max_isl, max_osl = 1, 256, 20 build_config = BuildConfig(max_input_len=max_isl, max_seq_len=max_osl + max_isl, @@ -69,12 +70,14 @@ def main(): engine = build(llama, build_config) engine.save(engine_dir) - executor = GenerationExecutor.create(engine_dir, tokenizer_dir) - + tokenizer = AutoTokenizer.from_pretrained(args.hf_model_dir) + executor = GenerationExecutor.create(engine_dir) sampling_params = SamplingParams(max_new_tokens=20) for inp in read_input(): - output = executor.generate(inp, sampling_params=sampling_params) - print(f">{output.text}") + output = executor.generate(tokenizer.encode(inp), + sampling_params=sampling_params) + print(f"> {tokenizer.decode(output.outputs[0].token_ids)}") -main() +if __name__ == "__main__": + main() diff --git a/examples/mpt/requirements.txt b/examples/mpt/requirements.txt index 9cd6ac378..7800b0ea6 100644 --- a/examples/mpt/requirements.txt +++ b/examples/mpt/requirements.txt @@ -1,5 +1,5 @@ --extra-index-url https://pypi.nvidia.com -tensorrt_llm==0.11.0.dev2024062500 +tensorrt_llm==0.12.0.dev2024070200 datasets~=2.14.5 evaluate~=0.4.1 rouge_score~=0.1.2 diff --git a/examples/multimodal/README.md b/examples/multimodal/README.md index ad31e50ed..38166a793 100644 --- a/examples/multimodal/README.md +++ b/examples/multimodal/README.md @@ -7,8 +7,7 @@ Multimodal models' LLM part has an additional parameter `--max_multimodal_len` c We first describe how to run each model on a single GPU. We then provide general guidelines on using tensor parallelism for the LLM part of the pipeline. -- [BLIP2-T5](#blip2-t5) -- [BLIP2-OPT](#blip2-opt) +- [BLIP2](#blip2) - [CogVLM](#cogvlm) - [Deplot](#deplot) - [Fuyu](#fuyu) @@ -20,29 +19,55 @@ We first describe how to run each model on a single GPU. We then provide general - [Video NeVA](#video-neva) - [Enabling tensor parallelism for multi-GPU](#enabling-tensor-parallelism-for-multi-gpu) -## BLIP2-T5 +## BLIP2 + +This BLIP section covers both BLIP2-OPT and BLIP2-T5, with minor changes needed when switching the LLM backbone. 1. Download Huggingface weights and convert original checkpoint to TRT-LLM checkpoint format - following example in `examples/enc_dec/README.md`. + following example in `examples/opt/README.md` and `examples/enc_dec/README.md`. ```bash - export MODEL_NAME="flan-t5-xl" # also flan-t5-xxl - git clone https://huggingface.co/google/${MODEL_NAME} tmp/hf_models/${MODEL_NAME} + export MODEL_NAME="blip2-opt-2.7b" # options: blip2-opt-6.7b, blip2-flan-t5-xl, blip2-flan-t5-xxl + git clone https://huggingface.co/Salesforce/${MODEL_NAME} tmp/hf_models/${MODEL_NAME} + ``` + + For BLIP2-OPT family, + ```bash + python ../opt/convert_checkpoint.py --model_type blip2 \ + --model_dir tmp/hf_models/${MODEL_NAME} \ + --output_dir tmp/trt_models/${MODEL_NAME}/fp16/1-gpu \ + --dtype float16 + ``` - python ../enc_dec/convert_checkpoint.py --model_type t5 \ + For BLIP2-T5 family, + ```bash + python ../enc_dec/convert_checkpoint.py --model_type blip2 \ --model_dir tmp/hf_models/${MODEL_NAME} \ --output_dir tmp/trt_models/${MODEL_NAME}/bfloat16 \ --tp_size 1 \ --pp_size 1 \ - --dtype bfloat16 \ - --max_multimodal_len 256 # 8 (max_batch_size) * 32 (num_visual_features) + --dtype bfloat16 ``` 2. Build TRT-LLM engine from TRT-LLM checkpoint + For BLIP2-OPT family, + ```bash + trtllm-build \ + --checkpoint_dir tmp/trt_models/${MODEL_NAME}/fp16/1-gpu \ + --output_dir trt_engines/${MODEL_NAME}/fp16/1-gpu \ + --gemm_plugin float16 \ + --max_beam_width 1 \ + --max_batch_size 8 \ + --max_seq_len 1024 \ + --max_input_len 924 \ + --max_multimodal_len 256 # 8 (max_batch_size) * 32 (num_visual_features) + ``` + + For BLIP2-T5 family, ```bash trtllm-build --checkpoint_dir tmp/trt_models/${MODEL_NAME}/bfloat16/encoder \ - --output_dir tmp/trt_engines/${MODEL_NAME}/1-gpu/bfloat16/encoder \ + --output_dir tmp/trt_engines/${MODEL_NAME}/bfloat16/encoder \ --paged_kv_cache disable \ --moe_plugin disable \ --enable_xqa disable \ @@ -54,13 +79,11 @@ We first describe how to run each model on a single GPU. We then provide general --context_fmha disable \ --max_beam_width 1 \ --max_batch_size 8 \ - --max_seq_len 1024 \ --max_input_len 924 \ --max_multimodal_len 256 # 8 (max_batch_size) * 32 (num_visual_features) - # Same command for decoder but don't set --max_multimodal_len trtllm-build --checkpoint_dir tmp/trt_models/${MODEL_NAME}/bfloat16/decoder \ - --output_dir tmp/trt_engines/${MODEL_NAME}/1-gpu/bfloat16/decoder \ + --output_dir tmp/trt_engines/${MODEL_NAME}/bfloat16/decoder \ --paged_kv_cache disable \ --moe_plugin disable \ --enable_xqa disable \ @@ -74,17 +97,15 @@ We first describe how to run each model on a single GPU. We then provide general --max_batch_size 8 \ --max_seq_len 1024 \ --max_encoder_input_len 924 \ - --max_input_len 1 + --max_input_len 1 # Same command for decoder but don't set --max_multimodal_len ``` **NOTE**: `max_multimodal_len = max_batch_size * num_visual_features`, so if you change max_batch_size, max multimodal length **MUST** be changed accordingly. - The built T5 engines are located in `./tmp/trt_engines/${MODEL_NAME}/1-gpu/bfloat16`. - -3. Build TensorRT engines for visual components +3. Build TensorRT engines for vision encoders ```bash - python build_visual_engine.py --model_type ${MODEL_NAME} --model_path tmp/hf_models/${MODEL_NAME} --max_batch_size 8 + python build_visual_engine.py --model_type blip2 --model_path tmp/hf_models/${MODEL_NAME} --max_batch_size 8 ``` The built engines are located in `./visual_engines/${MODEL_NAME}`. @@ -93,55 +114,27 @@ We first describe how to run each model on a single GPU. We then provide general 4. Assemble everything into BLIP2 pipeline + For BLIP2-OPT family, ```bash python run.py \ --max_new_tokens 30 \ --input_text "Question: which city is this? Answer:" \ --hf_model_dir tmp/hf_models/${MODEL_NAME} \ --visual_engine_dir visual_engines/${MODEL_NAME} \ - --llm_engine_dir tmp/trt_engines/${MODEL_NAME}/1-gpu/bfloat16 + --llm_engine_dir trt_engines/${MODEL_NAME}/fp16/1-gpu ``` -## BLIP2-OPT - -OPT pipeline needs few minor changes from T5 pipeline - -1. Convert Huggingface weights to TRT-LLM checkpoint format following `examples/opt/README.md`. - -2. Use `trtllm-build` command to build TRT-LLM engine for OPT. - -3. The full list of commands is as follows: - + For BLIP2-T5 family, ```bash - export MODEL_NAME="opt-2.7b" # also opt-6.7b - git clone https://huggingface.co/facebook/${MODEL_NAME} tmp/hf_models/${MODEL_NAME} - - python ../opt/convert_checkpoint.py \ - --model_dir tmp/hf_models/${MODEL_NAME} \ - --dtype float16 \ - --output_dir tmp/trt_models/${MODEL_NAME}/fp16/1-gpu - - trtllm-build \ - --checkpoint_dir tmp/trt_models/${MODEL_NAME}/fp16/1-gpu \ - --output_dir trt_engines/${MODEL_NAME}/fp16/1-gpu \ - --gemm_plugin float16 \ - --max_beam_width 1 \ - --max_batch_size 8 \ - --max_multimodal_len 256 \ - --max_input_len 924 \ - --max_seq_len 1024 - - python build_visual_engine.py --model_type ${MODEL_NAME} --model_path tmp/hf_models/${MODEL_NAME} - python run.py \ --max_new_tokens 30 \ --input_text "Question: which city is this? Answer:" \ --hf_model_dir tmp/hf_models/${MODEL_NAME} \ --visual_engine_dir visual_engines/${MODEL_NAME} \ - --llm_engine_dir trt_engines/${MODEL_NAME}/fp16/1-gpu + --llm_engine_dir tmp/trt_engines/${MODEL_NAME}/bfloat16 ``` -4. INT8/INT4 weight-only quantization for OPT can be enabled using commands as follows (take `INT4` as an example, while `INT8` is the default precision for weight-only quantization): +5. (Optional) INT8/INT4 weight-only quantization for OPT can be enabled using commands as follows (take `INT4` as an example, while `INT8` is the default precision for weight-only quantization): ```bash python ../opt/convert_checkpoint.py \ --model_dir tmp/hf_models/${MODEL_NAME} \ @@ -640,7 +633,7 @@ Currently, CogVLM only support bfloat16 precision and doesn't support `remove_in python run.py \ --hf_model_dir tmp/hf_models/${MODEL_NAME} \ --visual_engine_dir visual_engines/${MODEL_NAME} \ - --llm_engine_dir tmp/trt_engines/${MODEL_NAME}/1-gpu/bfloat16 \ + --llm_engine_dir tmp/trt_engines/${MODEL_NAME}/1-gpu/bfloat16 ``` Note: Nougat models usually do not need a text prompt. @@ -681,7 +674,8 @@ Currently, CogVLM only support bfloat16 precision and doesn't support `remove_in python run.py \ --hf_model_dir tmp/hf_models/${MODEL_NAME} \ --visual_engine_dir visual_engines/${MODEL_NAME} \ - --llm_engine_dir trt_engines/${MODEL_NAME}/fp16/1-gpu/ + --llm_engine_dir trt_engines/${MODEL_NAME}/fp16/1-gpu/ \ + --image_path=https://storage.googleapis.com/sfr-vision-language-research/LAVIS/assets/merlion.png ``` ## Video NeVA diff --git a/examples/multimodal/build_visual_engine.py b/examples/multimodal/build_visual_engine.py index e4608c0bf..440d8572f 100644 --- a/examples/multimodal/build_visual_engine.py +++ b/examples/multimodal/build_visual_engine.py @@ -33,10 +33,9 @@ def parse_arguments(): type=str, default=None, choices=[ - 'opt-2.7b', 'opt-6.7b', 'flan-t5-xl', 'flan-t5-xxl', - 'llava', 'vila', 'nougat', 'cogvlm', 'fuyu', - 'pix2struct', 'neva', 'kosmos-2', 'video-neva', - 'phi-3-vision' + 'blip2', 'llava', 'vila', 'nougat', 'cogvlm', + 'fuyu', 'pix2struct', 'neva', 'kosmos-2', + 'video-neva', 'phi-3-vision' ], help="Model type") parser.add_argument( @@ -77,7 +76,7 @@ def __init__(self, args): def build(self): args = self.args - if 'opt' in args.model_type or 't5' in args.model_type: + if args.model_type == 'blip2': build_blip2_engine(args) elif args.model_type == 'pix2struct': build_pix2struct_engine(args) @@ -208,8 +207,7 @@ def build_trt_engine(model_type, def build_blip2_engine(args): - model_type = 'Salesforce/blip2-' + args.model_type - processor = Blip2Processor.from_pretrained(model_type) + processor = Blip2Processor.from_pretrained(args.model_path) raw_image = Image.new('RGB', [10, 10]) # dummy image prompt = "Question: what is this? Answer:" @@ -234,14 +232,22 @@ def forward(self, image): return self.projector(qformer_output.last_hidden_state) model = Blip2ForConditionalGeneration.from_pretrained( - model_type, torch_dtype=torch.float16) + args.model_path, torch_dtype=torch.float16) + + blip2_llm = "" + if model.language_model.config.architectures[ + 0] == 'T5ForConditionalGeneration': + blip2_llm = "t5" + elif model.language_model.config.architectures[0] == 'OPTForCausalLM': + blip2_llm = "opt" + wrapper = Blip2VisionWrapper(model.vision_model, model.qformer, model.language_projection, model.query_tokens) wrapper.to(args.device) export_visual_wrapper_onnx(wrapper, image, args.output_dir) build_trt_engine( - model_type, + args.model_type + "-" + blip2_llm, # blip2-t5 or blip2-opt [image.shape[1], image.shape[2], image.shape[3]], # [3, H, W] args.output_dir, args.max_batch_size) diff --git a/examples/multimodal/run.py b/examples/multimodal/run.py index 14c953ae6..8c55905de 100644 --- a/examples/multimodal/run.py +++ b/examples/multimodal/run.py @@ -760,16 +760,22 @@ def load_image(image_path): elif "video-neva" in self.model_type: image = args.video_path else: - img_url = 'https://storage.googleapis.com/sfr-vision-language-research/LAVIS/assets/merlion.png' - image = Image.open(requests.get(img_url, - stream=True).raw).convert('RGB') + img_url = args.image_path + if img_url is None: + img_url = 'https://storage.googleapis.com/sfr-vision-language-research/LAVIS/assets/merlion.png' + + if img_url.startswith("http") or img_url.startswith("https"): + image = Image.open(requests.get(img_url, + stream=True).raw).convert('RGB') + else: + image = Image.open(img_url).convert("RGB") return image def setup_inputs(self, input_text, raw_image): attention_mask = None if 'blip2' in self.model_type: - processor = Blip2Processor.from_pretrained(self.model_type) + processor = Blip2Processor.from_pretrained(self.args.hf_model_dir) image = processor(raw_image, input_text, return_tensors="pt")['pixel_values'] @@ -934,9 +940,12 @@ def setup_inputs(self, input_text, raw_image): decoder_input_ids = None else: config = AutoConfig.from_pretrained(args.hf_model_dir) - decoder_start_id = config.decoder_start_token_id # T5 - if decoder_start_id is None: + if "blip2" in self.model_type: + decoder_start_id = config.text_config.decoder_start_token_id # T5 + elif "nougat" in self.model_type: decoder_start_id = config.decoder.bos_token_id # Nougat + else: + decoder_start_id = config.decoder_start_token_id decoder_input_ids = torch.IntTensor([[decoder_start_id]]) decoder_input_ids = decoder_input_ids.repeat((args.batch_size, 1)) @@ -990,7 +999,7 @@ def print_result(self, input_text, output_text): elif self.model_type == "pix2struct": assert "characteristic | cat food, day | cat food, wet | cat treats" in output_text[ 0][0].lower() - elif self.model_type in ['neva', 'phi-3-vision']: + elif self.model_type in ['blip2', 'neva', 'phi-3-vision']: assert 'singapore' in output_text[0][0].lower() elif self.model_type == 'video-neva': assert 'robot' in output_text[0][0].lower() diff --git a/examples/nemotron/requirements.txt b/examples/nemotron/requirements.txt index 6e9c7e8f7..baeae63c0 100644 --- a/examples/nemotron/requirements.txt +++ b/examples/nemotron/requirements.txt @@ -1,5 +1,5 @@ --extra-index-url https://pypi.nvidia.com -tensorrt_llm==0.11.0.dev2024062500 +tensorrt_llm==0.12.0.dev2024070200 transformers==4.40.2 datasets~=2.14.5 evaluate~=0.4.1 diff --git a/examples/openai_triton/manual_plugin/README.md b/examples/openai_triton/manual_plugin/README.md index 149d02d05..40e6f124b 100644 --- a/examples/openai_triton/manual_plugin/README.md +++ b/examples/openai_triton/manual_plugin/README.md @@ -145,3 +145,33 @@ It may be resolved by reduing the block size. ### 3. AttributeError: module 'triton' has no attribute 'jit' This problem may arise if Triton is installed in editable mode. To resolve this issue, please install Triton using the non-editable mode. Refer https://github.com/openai/triton/issues/1693. + +### 4. Unload the same module more than once while building the engine +When the plugin is used more than once within a model, the function cuModuleUnload() will be invoked multiple times during the engine building stage. Related code is generated by Openai Triton and can be found in the folder `examples/openai_triton/manual_plugin/aot/`. One example is: + +```c++ +void unload_fmha_d64_fp32_f30323ef_0d1d2d3d4d5d6789(void) { + CUDA_CHECK(cuModuleUnload(fmha_d64_fp32_f30323ef_0d1d2d3d4d5d6789_mod)); +} +``` + +As the generated code didn't check the value of the module object, this function might unload the same module multiple times, which will cause an error as follows: + +``` +Triton Error [CUDA]: invalid resource handle\n/opt/rapids/src/cudf/cpp/build/_deps/arrow-src/cpp/src/arrow/filesystem/s3fs.cc:2904:  arrow::fs::FinalizeS3 was not called even though S3 was initialized.  This could lead to a segmentation fault at exit +``` + +The error message is ambiguous. If we use compute-sanitizer to help debug, we can get the following information: +``` +========= Program hit CUDA_ERROR_INVALID_HANDLE (error 400) due to "invalid resource handle" on CUDA API call to cuModuleUnload. +``` + +So we need to modify the above generated code as follows to avoid the above error. +```c++ +void unload_fmha_d64_fp32_f30323ef_0d1d2d3d4d5d6789(void) { + if(fmha_d64_fp32_f30323ef_0d1d2d3d4d5d6789_mod){ + CUDA_CHECK(cuModuleUnload(fmha_d64_fp32_f30323ef_0d1d2d3d4d5d6789_mod)); + } + fmha_d64_fp32_f30323ef_0d1d2d3d4d5d6789_mod=NULL; +} +``` diff --git a/examples/opt/convert_checkpoint.py b/examples/opt/convert_checkpoint.py index 01994d813..5d075abb1 100644 --- a/examples/opt/convert_checkpoint.py +++ b/examples/opt/convert_checkpoint.py @@ -7,7 +7,7 @@ import safetensors import torch -from transformers import AutoModelForCausalLM +from transformers import AutoModelForCausalLM, Blip2ForConditionalGeneration import tensorrt_llm from tensorrt_llm._utils import pad_vocab_size @@ -17,6 +17,13 @@ def parse_arguments(): parser = argparse.ArgumentParser() parser.add_argument('--model_dir', type=str, default=None) + parser.add_argument( + '--model_type', + type=str, + default='opt', + choices=['opt', 'blip2'], + help= + 'Multimodal type when this script is used for multimodal conversion.') parser.add_argument('--tp_size', type=int, default=1, @@ -318,8 +325,13 @@ def convert_hf_opt(hf_model, if not os.path.exists(args.output_dir): os.makedirs(args.output_dir) - hf_model = AutoModelForCausalLM.from_pretrained(args.model_dir, - torch_dtype="auto") + if args.model_type == 'opt': + hf_model = AutoModelForCausalLM.from_pretrained(args.model_dir, + torch_dtype="auto") + elif args.model_type == 'blip2': + hf_model = Blip2ForConditionalGeneration.from_pretrained( + args.model_dir, torch_dtype="auto").language_model + hf_config = hf_model.config if hf_config.hidden_size != hf_config.word_embed_proj_dim: args.use_embedding_sharing = False diff --git a/examples/opt/requirements.txt b/examples/opt/requirements.txt index 9cd6ac378..7800b0ea6 100644 --- a/examples/opt/requirements.txt +++ b/examples/opt/requirements.txt @@ -1,5 +1,5 @@ --extra-index-url https://pypi.nvidia.com -tensorrt_llm==0.11.0.dev2024062500 +tensorrt_llm==0.12.0.dev2024070200 datasets~=2.14.5 evaluate~=0.4.1 rouge_score~=0.1.2 diff --git a/examples/phi/README.md b/examples/phi/README.md index a9ee4f2c4..6893075a8 100644 --- a/examples/phi/README.md +++ b/examples/phi/README.md @@ -12,6 +12,8 @@ models using TensorRT-LLM and run on a single GPU. - [2. Build TensorRT engine(s)](#2-build-tensorrt-engines) - [Fused MultiHead Attention (FMHA)](#fused-multihead-attention-fmha) - [3. Summarization using the Phi model](#3-summarization-using-the-phi-model) + - [4. Quantization](#4-quantization) + - [5. Run Phi-3 with LoRA](#5-run-phi-3-with-lora) ## Overview @@ -25,12 +27,6 @@ In addition, there are two shared files in the parent folder [`examples`](../) f * [`../summarize.py`](../summarize.py) to summarize the articles in the [cnn_dailymail](https://huggingface.co/datasets/cnn_dailymail) dataset. ## Support Matrix - * FP16 - * BF16 - * FP8 - * INT8 - * Tensor Parallel - ## Support Matrix | Model Name | FP16 | BF16 | FP8 | INT8 | TP | | :--------------: | :---: | :---: | :---: | :---: | :---: | @@ -128,7 +124,7 @@ python3 ../summarize.py --engine_dir ./phi-engine-tp2 \ ``` -### 5. Quantization +### 4. Quantization All Phi-3 variants support post-training quantization to FP8 and INT8 SmoothQuant formats. @@ -156,3 +152,44 @@ python3 ../quantization/quantize.py \ The commands to [build TensorRT engines](#2-build-tensorrt-engines) from quantized checkpoints and to run [summarization test](#3-summarization-using-the-phi-model) are same as those for unquantized checkpoints. + +### 5. Run Phi-3 with LoRA + +TensorRT-LLM supports running Phi-3-mini/small models with FP16/BF16/FP32 LoRA. In this section, we use Phi-3-mini as an example to show how to run an FP8 base model with FP16 LoRA module. + +* download the base model and lora model from HF + +```bash +git-lfs clone https://huggingface.co/microsoft/Phi-3-mini-4k-instruct +git-lfs clone https://huggingface.co/sikoraaxd/Phi-3-mini-4k-instruct-ru-lora +``` + +* Quantize the Phi-3-mini model to fp8 from HF +```bash +BASE_PHI_3_MINI_MODEL=./Phi-3-mini-4k-instruct +python ../quantization/quantize.py --model_dir ${BASE_PHI_3_MINI_MODEL} \ + --dtype float16 \ + --qformat fp8 \ + --kv_cache_dtype fp8 \ + --output_dir phi3_mini_4k_instruct/trt_ckpt/fp8/1-gpu \ + --calib_size 512 +``` + +* Build engine and run inference. +``` +trtllm-build --checkpoint_dir phi3_mini_4k_instruct/trt_ckpt/fp8/1-gpu \ + --output_dir phi3_mini_4k_instruct/trt_engines/fp8_lora/1-gpu \ + --gemm_plugin auto \ + --max_batch_size 8 \ + --max_input_len 1024 \ + --max_seq_len 2048 \ + --lora_plugin auto \ + --lora_dir ./Phi-3-mini-4k-instruct-ru-lora + +python ../run.py --engine_dir phi3_mini_4k_instruct/trt_engines/fp8_lora/1-gpu \ + --max_output_len 500 \ + --tokenizer_dir ./Phi-3-mini-4k-instruct-ru-lora \ + --input_text "<|user|>\nCan you provide ways to eat combinations of bananas and dragonfruits?<|end|>\n<|assistant|>" \ + --lora_task_uids 0 \ + --use_py_session +``` diff --git a/examples/phi/requirements.txt b/examples/phi/requirements.txt index 07d57c486..065885745 100644 --- a/examples/phi/requirements.txt +++ b/examples/phi/requirements.txt @@ -1,5 +1,5 @@ --extra-index-url https://pypi.nvidia.com -tensorrt_llm==0.11.0.dev2024062500 +tensorrt_llm==0.12.0.dev2024070200 datasets~=2.14.5 evaluate~=0.4.1 rouge_score~=0.1.2 diff --git a/examples/quantization/requirements.txt b/examples/quantization/requirements.txt index 98ab003b8..1d8afb40a 100644 --- a/examples/quantization/requirements.txt +++ b/examples/quantization/requirements.txt @@ -1,5 +1,5 @@ --extra-index-url https://pypi.nvidia.com -tensorrt_llm==0.11.0.dev2024062500 +tensorrt_llm==0.12.0.dev2024070200 datasets>=2.14.4 nemo-toolkit[all]<=1.20.0,>=1.18.0 rouge_score~=0.1.2 diff --git a/examples/qwen/README.md b/examples/qwen/README.md index 4ba54bada..9d6dd06e5 100644 --- a/examples/qwen/README.md +++ b/examples/qwen/README.md @@ -14,6 +14,7 @@ This document shows how to build and run a [Qwen](https://huggingface.co/Qwen) m - [INT4-GPTQ](#int4-gptq) - [INT4-AWQ](#int4-awq) - [Run](#run) + - [Run models with LoRA](#run-models-with-lora) - [Summarization using the Qwen model](#summarization-using-the-qwen-model) - [Credits](#credits) @@ -411,6 +412,72 @@ What is your name?<|im_end|> Output [Text 0 Beam 0]: "I am QianWen, a large language model created by Alibaba Cloud." ``` +### Run models with LoRA + +Download the lora model from HF: + +```bash +git clone https://huggingface.co/Jungwonchang/Ko-QWEN-7B-Chat-LoRA ./tmp/Ko-QWEN-7B-Chat-LoRA +``` + +Build engine, setting `--lora_plugin` and `--lora_dir`. If lora has separate lm_head and embedding, they will replace lm_head and embedding of base model. + +```bash +python convert_checkpoint.py --model_dir ./tmp/Qwen/7B/ \ + --output_dir ./tllm_checkpoint_1gpu_fp16 \ + --dtype float16 + +trtllm-build --checkpoint_dir ./tllm_checkpoint_1gpu_fp16 \ + --output_dir ./tmp/qwen/7B_lora/trt_engines/fp16/1-gpu \ + --gemm_plugin auto \ + --lora_plugin auto \ + --lora_dir ./tmp/Ko-QWEN-7B-Chat-LoRA +``` + +Run inference: + +```bash +python ../run.py --engine_dir ./tmp/qwen/7B_lora/trt_engines/fp16/1-gpu \ + --max_output_len 50 \ + --tokenizer_dir ./tmp/Qwen/7B/ \ + --input_text "안녕하세요, 혹시 이름이 뭐에요?" \ + --lora_task_uids 0 \ + --use_py_session + +Input [Text 0]: "<|im_start|>system +You are a helpful assistant.<|im_end|> +<|im_start|>user +안녕하세요, 혹시 이름이 뭐에요?<|im_end|> +<|im_start|>assistant +" +Output [Text 0 Beam 0]: "안녕하세요! 저는 인공지능 어시스턴트로, 여러분의 질문에 답하고 도움을 드리기 위해 여기 있습니다. 제가 무엇을 도와드릴까요?<|im_end|> +<|im_start|>0 +<|im_start|><|im_end|> +<|im_start|>" +``` + +Users who want to skip LoRA module may pass uid -1 with `--lora_task_uids -1`. +In that case, the model will not run the LoRA module and the results will be +different. + +```bash +python ../run.py --engine_dir ./tmp/qwen/7B_lora/trt_engines/fp16/1-gpu \ + --max_output_len 50 \ + --tokenizer_dir ./tmp/Qwen/7B/ \ + --input_text "안녕하세요, 혹시 이름이 뭐에요?" \ + --lora_task_uids -1 \ + --use_py_session + +Input [Text 0]: "<|im_start|>system +You are a helpful assistant.<|im_end|> +<|im_start|>user +안녕하세요, 혹시 이름이 뭐에요?<|im_end|> +<|im_start|>assistant +" +Output [Text 0 Beam 0]: "안녕하세요! 저는 "QianWen"입니다.<|im_end|> +" +``` + ### Summarization using the Qwen model ```bash diff --git a/examples/qwen/requirements.txt b/examples/qwen/requirements.txt index 80a4f7c70..ea746afdd 100644 --- a/examples/qwen/requirements.txt +++ b/examples/qwen/requirements.txt @@ -1,5 +1,5 @@ --extra-index-url https://pypi.nvidia.com -tensorrt_llm==0.11.0.dev2024062500 +tensorrt_llm==0.12.0.dev2024070200 datasets~=2.16.0 evaluate~=0.4.1 rouge_score~=0.1.2 diff --git a/examples/qwenvl/requirements.txt b/examples/qwenvl/requirements.txt index 0dc21bb2d..4440b456a 100644 --- a/examples/qwenvl/requirements.txt +++ b/examples/qwenvl/requirements.txt @@ -1,5 +1,5 @@ --extra-index-url https://pypi.nvidia.com -tensorrt_llm==0.11.0.dev2024062500 +tensorrt_llm==0.12.0.dev2024070200 datasets~=2.16.0 evaluate~=0.4.1 rouge_score~=0.1.2 diff --git a/examples/recurrentgemma/requirements.txt b/examples/recurrentgemma/requirements.txt index 6d8673322..9d81522ce 100644 --- a/examples/recurrentgemma/requirements.txt +++ b/examples/recurrentgemma/requirements.txt @@ -1,5 +1,5 @@ --extra-index-url https://pypi.nvidia.com -tensorrt_llm==0.11.0.dev2024062500 +tensorrt_llm==0.12.0.dev2024070200 git+https://github.com/google-deepmind/recurrentgemma.git flax>=0.8.2 jax~=0.4.23 diff --git a/examples/run.py b/examples/run.py index eb1313e57..80f8b603c 100644 --- a/examples/run.py +++ b/examples/run.py @@ -22,7 +22,8 @@ import numpy as np import torch from utils import (DEFAULT_HF_MODEL_DIRS, DEFAULT_PROMPT_TEMPLATES, - add_common_args, load_tokenizer, read_model_name, + add_common_args, load_tokenizer, read_decoder_start_token_id, + read_model_name, supports_inflight_batching, throttle_generator) import tensorrt_llm @@ -35,6 +36,7 @@ def parse_arguments(args=None): + # see `add_common_args` for extended list of arguments parser = argparse.ArgumentParser() parser.add_argument('--max_input_length', type=int, default=923) parser.add_argument('--max_output_len', type=int, required=True) @@ -287,15 +289,26 @@ def main(args): if is_enc_dec: encoder_input_ids = batch_input_ids + decoder_start_token_id = read_decoder_start_token_id( + os.path.join(args.engine_dir, "decoder")) decoder_input_ids = [ - torch.tensor([pad_id], dtype=torch.int32) for _ in batch_input_ids - ] # by default decoder_start_token_id for T5 + torch.tensor([decoder_start_token_id], dtype=torch.int32) + for _ in batch_input_ids + ] input_lengths = [x.size(0) for x in decoder_input_ids ] if is_enc_dec else [x.size(0) for x in batch_input_ids] encoder_input_lengths = [x.size(0) for x in encoder_input_ids] if is_enc_dec else None + if not supports_inflight_batching( + os.path.join(args.engine_dir, "decoder") if is_enc_dec else args. + engine_dir): + logger.warning( + "The given engine does not support in-flight batching, fallback to python session" + ) + args.use_py_session = True + if not PYTHON_BINDINGS and not args.use_py_session: logger.warning( "Python bindings of C++ session is unavailable, fallback to Python session." @@ -306,6 +319,18 @@ def main(args): "Debug mode is not supported in C++ session for now, fallback to Python session." ) args.use_py_session = True + if args.return_all_generated_tokens and args.use_py_session: + raise ValueError( + "Returning all the generated tokens at each step is not supported in the Python session, use C++ session instead." + ) + if (not args.return_all_generated_tokens) and args.streaming and ( + args.num_beams > 1): + logger.warning( + "Setting return_all_generated_tokens to True since streaming AND beam search are done simultaneously. " + "Returning the full beams at each streaming step is needed because beam search + streaming can change previous outputs. " + "WARNING: using this option may increase network usage significantly (quadratically w.r.t output length)." + ) + args.return_all_generated_tokens = True runner_cls = ModelRunner if args.use_py_session else ModelRunnerCpp runner_kwargs = dict( engine_dir=args.engine_dir, @@ -335,8 +360,7 @@ def main(args): kv_cache_enable_block_reuse=args.kv_cache_enable_block_reuse, kv_cache_free_gpu_memory_fraction=args. kv_cache_free_gpu_memory_fraction, - enable_chunked_context=args.enable_chunked_context, - ) + enable_chunked_context=args.enable_chunked_context) runner = runner_cls.from_dir(**runner_kwargs) with torch.no_grad(): @@ -370,7 +394,8 @@ def main(args): output_sequence_lengths=True, no_repeat_ngram_size=args.no_repeat_ngram_size, return_dict=True, - medusa_choices=args.medusa_choices) + medusa_choices=args.medusa_choices, + return_all_generated_tokens=args.return_all_generated_tokens) torch.cuda.synchronize() if args.streaming: @@ -457,7 +482,9 @@ def main(args): prompt_tasks=args.prompt_tasks, streaming=args.streaming, output_sequence_lengths=True, - return_dict=True) + return_dict=True, + return_all_generated_tokens=args.return_all_generated_tokens + ) torch.cuda.synchronize() tensorrt_llm.profiler.start("tmp") @@ -489,7 +516,9 @@ def main(args): prompt_tasks=args.prompt_tasks, streaming=args.streaming, output_sequence_lengths=True, - return_dict=True) + return_dict=True, + return_all_generated_tokens=args.return_all_generated_tokens + ) torch.cuda.synchronize() tensorrt_llm.profiler.stop("tmp") diff --git a/examples/skywork/requirements.txt b/examples/skywork/requirements.txt index a2bbe457e..b56872e4f 100644 --- a/examples/skywork/requirements.txt +++ b/examples/skywork/requirements.txt @@ -1,5 +1,5 @@ --extra-index-url https://pypi.nvidia.com -tensorrt_llm==0.11.0.dev2024062500 +tensorrt_llm==0.12.0.dev2024070200 datasets~=2.16.1 evaluate~=0.4.1 rouge_score~=0.1.2 diff --git a/examples/smaug/requirements.txt b/examples/smaug/requirements.txt index 814a8c824..f9f50349b 100644 --- a/examples/smaug/requirements.txt +++ b/examples/smaug/requirements.txt @@ -1,5 +1,5 @@ --extra-index-url https://pypi.nvidia.com -tensorrt_llm==0.11.0.dev2024062500 +tensorrt_llm==0.12.0.dev2024070200 datasets==2.14.6 evaluate~=0.4.1 rouge_score~=0.1.2 diff --git a/examples/summarize.py b/examples/summarize.py index 33405d405..2be8c4703 100644 --- a/examples/summarize.py +++ b/examples/summarize.py @@ -25,7 +25,7 @@ from transformers import (AutoModel, AutoModelForCausalLM, AutoModelForSeq2SeqLM, GenerationConfig) from utils import (DEFAULT_HF_MODEL_DIRS, add_common_args, load_tokenizer, - read_model_name) + read_model_name, supports_inflight_batching) import tensorrt_llm import tensorrt_llm.profiler as profiler @@ -404,11 +404,21 @@ def eval_hf(datapoint, return output_lines_list, tokens_list, ppls if test_trt_llm: + if not supports_inflight_batching(args.engine_dir): + logger.warning( + "The given engine does not support in-flight batching, fallback to python session" + ) + args.use_py_session = True + if not PYTHON_BINDINGS and not args.use_py_session: logger.warning( "Python bindings of C++ session is unavailable, fallback to Python session." ) args.use_py_session = True + if args.return_all_generated_tokens: + raise ValueError( + "Returning all the generated tokens at each step is not supported in summarize.py" + ) runner_cls = ModelRunner if args.use_py_session else ModelRunnerCpp runner_kwargs = dict(engine_dir=args.engine_dir, rank=runtime_rank, diff --git a/examples/utils.py b/examples/utils.py index 7628cb978..2305ef0e1 100644 --- a/examples/utils.py +++ b/examples/utils.py @@ -18,6 +18,7 @@ from transformers import AutoTokenizer, LlamaTokenizer, T5Tokenizer +from tensorrt_llm.bindings import GptJsonConfig from tensorrt_llm.builder import get_engine_version DEFAULT_HF_MODEL_DIRS = { @@ -54,6 +55,19 @@ } +def supports_inflight_batching(engine_dir): + config_path = Path(engine_dir) / "config.json" + json_config = GptJsonConfig.parse_file(config_path) + model_config = json_config.model_config + return model_config.supports_inflight_batching + + +def read_decoder_start_token_id(engine_dir): + with open(Path(engine_dir) / "config.json", 'r') as f: + config = json.load(f) + return config['pretrained_config']['decoder_start_token_id'] + + def read_model_name(engine_dir: str): engine_version = get_engine_version(engine_dir) @@ -315,4 +329,16 @@ def add_common_args(parser): action='store_true', help="Use device map 'auto' to load a pretrained HF model. This may " "help to test a large model that cannot fit into a singlue GPU.") + + parser.add_argument( + "--return_all_generated_tokens", + default=False, + action="store_true", + help="if false, return only generated tokens at each streaming step." + "If true, return the full beams/outputs at each step" + "Overwritten to True if num_beams>1 and streaming" + "(only available with cpp session). " + "WARNING: using this option may increase network usage significantly (quadratically w.r.t output length)." + ) + return parser diff --git a/examples/whisper/README.md b/examples/whisper/README.md index 380543c36..6766607f0 100755 --- a/examples/whisper/README.md +++ b/examples/whisper/README.md @@ -70,7 +70,8 @@ trtllm-build --checkpoint_dir ${checkpoint_dir}/encoder \ --max_batch_size ${MAX_BATCH_SIZE} \ --gemm_plugin disable \ --bert_attention_plugin ${INFERENCE_PRECISION} \ - --remove_input_padding disable + --remove_input_padding disable \ + --max_input_len 1500 trtllm-build --checkpoint_dir ${checkpoint_dir}/decoder \ --output_dir ${output_dir}/decoder \ @@ -142,7 +143,8 @@ trtllm-build --checkpoint_dir ${checkpoint_dir}/encoder \ --max_batch_size ${MAX_BATCH_SIZE} \ --gemm_plugin disable \ --bert_attention_plugin ${INFERENCE_PRECISION} \ - --remove_input_padding disable + --remove_input_padding disable \ + --max_input_len 1500 trtllm-build --checkpoint_dir ${checkpoint_dir}/decoder \ --output_dir ${output_dir}/decoder \ @@ -152,7 +154,7 @@ trtllm-build --checkpoint_dir ${checkpoint_dir}/decoder \ --use_custom_all_reduce disable \ --max_beam_width ${MAX_BEAM_WIDTH} \ --max_batch_size ${MAX_BATCH_SIZE} \ - --max_output_len 100 \ + --max_seq_len 114 \ --max_input_len 14 \ --max_encoder_input_len 1500 \ --gemm_plugin ${INFERENCE_PRECISION} \ diff --git a/examples/whisper/requirements.txt b/examples/whisper/requirements.txt index b2a1ae5bc..7f7b9e603 100644 --- a/examples/whisper/requirements.txt +++ b/examples/whisper/requirements.txt @@ -1,5 +1,5 @@ --extra-index-url https://pypi.nvidia.com -tensorrt_llm==0.11.0.dev2024062500 +tensorrt_llm==0.12.0.dev2024070200 tiktoken datasets kaldialign diff --git a/requirements-windows.txt b/requirements-windows.txt index 762437b08..cb9f553d9 100644 --- a/requirements-windows.txt +++ b/requirements-windows.txt @@ -6,7 +6,7 @@ cuda-python==12.4.0 diffusers==0.27.0 numpy<2 onnx>=1.12.0 -polygraphy +polygraphy==0.49.9 psutil pynvml>=11.5.0 pulp @@ -15,11 +15,11 @@ h5py==3.10.0 pywin32 StrEnum sentencepiece>=0.1.99 -tensorrt==10.0.1 +tensorrt-cu12==10.1.0 tokenizers>=0.14 # Default torch is CPU-only on Windows, so need to specify a torch version with GPU support -torch @ https://download.pytorch.org/whl/cu121/torch-2.2.0%2Bcu121-cp310-cp310-win_amd64.whl#sha256=8f54c647ee19c8b4c0aad158c73b83b2c06cb62351e9cfa981540ce7295a9015 -nvidia-modelopt~=0.11,<0.12 +torch @ https://download.pytorch.org/whl/cu121/torch-2.3.1%2Bcu121-cp310-cp310-win_amd64.whl +nvidia-modelopt~=0.13,<0.14 transformers==4.38.2 wheel optimum diff --git a/requirements.txt b/requirements.txt index d5d157897..e8d06ef40 100644 --- a/requirements.txt +++ b/requirements.txt @@ -16,13 +16,14 @@ pandas h5py==3.10.0 StrEnum sentencepiece>=0.1.99 -tensorrt==10.1.0 +tensorrt-cu12==10.1.0 ; platform_machine == 'x86_64' +tensorrt==10.1.0 ; platform_machine == 'aarch64' # https://github.com/pytorch/pytorch/blob/v2.3.1/version.txt uses 2.3.0a0. # https://docs.nvidia.com/deeplearning/frameworks/pytorch-release-notes/rel-24-05.html#rel-24-05 uses 2.4.0a0. torch>=2.3.0a0,<=2.4.0a0 -nvidia-modelopt~=0.11,<0.12 +nvidia-modelopt~=0.13,<0.14 transformers>=4.38.2 -pillow==10.2.0 +pillow==10.3.0 wheel optimum evaluate diff --git a/tensorrt_llm/__init__.py b/tensorrt_llm/__init__.py index 50c8e9012..4eef01cde 100644 --- a/tensorrt_llm/__init__.py +++ b/tensorrt_llm/__init__.py @@ -45,7 +45,7 @@ def _add_trt_llm_dll_directory(): from .auto_parallel import AutoParallelConfig, auto_parallel from .builder import BuildConfig, Builder, BuilderConfig, build from .functional import Tensor, constant -from .hlapi.llm import LLM, ModelConfig +from .hlapi.llm import LLM, LlmArgs, SamplingParams from .logger import logger from .mapping import Mapping from .module import Module @@ -83,7 +83,9 @@ def _add_trt_llm_dll_directory(): 'quantization', 'tools', 'LLM', - 'ModelConfig', + 'LlmArgs', + 'SamplingParams', + 'KvCacheConfig', '__version__', ] diff --git a/tensorrt_llm/auto_parallel/cluster_info.py b/tensorrt_llm/auto_parallel/cluster_info.py index 635a05d93..09eeaee88 100644 --- a/tensorrt_llm/auto_parallel/cluster_info.py +++ b/tensorrt_llm/auto_parallel/cluster_info.py @@ -1,3 +1,4 @@ +import copy import re from dataclasses import dataclass, field from typing import Dict, Tuple, Union @@ -68,6 +69,23 @@ class ClusterInfo(DictConversion): "PCIe-5": 64, } +_templates = { + "H100-SXM": + dict( + inter_node_bw_per_device=50, + intra_node_bw_per_device=450, + intra_node_sharp=True, + memory_bw=3350, + math_throughput=MathThroughput( + int8=1979, + fp8=1979, + float16=989, + bfloat16=989, + float32=495, + ), + ), +} + cluster_infos = { # from https://www.nvidia.com/content/dam/en-zz/Solutions/Data-Center/a100/pdf/nvidia-a100-datasheet-us-nvidia-1758950-r4-web.pdf "A100-SXM-80GB": @@ -101,18 +119,18 @@ class ClusterInfo(DictConversion): # from https://resources.nvidia.com/en-us-tensor-core/nvidia-tensor-core-gpu-datasheet "H100-SXM": ClusterInfo( - inter_node_bw_per_device=50, - intra_node_bw_per_device=450, - intra_node_sharp=True, - memory_bw=3350, + **_templates["H100-SXM"], memory_budget_per_device=80, - math_throughput=MathThroughput( - int8=1979, - fp8=1979, - float16=989, - bfloat16=989, - float32=495, - ), + ), + "H100-SXM-64G": + ClusterInfo( + **_templates["H100-SXM"], + memory_budget_per_device=64, + ), + "H100-SXM-94G": + ClusterInfo( + **_templates["H100-SXM"], + memory_budget_per_device=94, ), "H100-PCIe": ClusterInfo( @@ -351,6 +369,12 @@ def is_32gb(): return "H100-SXM" else: return "H100-PCIe" + elif match("H100XS", device_name): + return "H100-SXM-64G" + elif match("H100XM", device_name): + return "H100-SXM" + elif match("H100XL", device_name): + return "H100-SXM-94G" elif match("L40S", device_name): return "L40S" elif match("L40", device_name): @@ -454,7 +478,7 @@ def infer_cluster_info() -> ClusterInfo: handle = pynvml.nvmlDeviceGetHandleByIndex(index) compute_cap = pynvml.nvmlDeviceGetCudaComputeCapability(handle) logger.info(f"Compute capability: {compute_cap}") - err, properties = cudart.cudaGetDeviceProperties(0) + err, properties = cudart.cudaGetDeviceProperties(index) sm_count = properties.multiProcessorCount logger.info(f"SM count: {sm_count}") sm_clock = pynvml.nvmlDeviceGetMaxClockInfo( @@ -533,9 +557,20 @@ def infer_cluster_config() -> Dict[str, Union[str, ClusterInfo]]: if cluster_key is not None: return dict(cluster_key=cluster_key) else: + try: + cluster_info = infer_cluster_info() + except pynvml.NVMLError: + fallback_cluster_key = "L40" + cluster_info = copy.copy(cluster_infos[fallback_cluster_key]) + memory_budget = torch.cuda.mem_get_info()[1] // (1024**3) + cluster_info.memory_budget_per_device = memory_budget + logger.warning( + f"Failed to infer cluster info for {device_name}, " + f"treat it as a {fallback_cluster_key} node with {memory_budget} GB memory. " + "This setting makes no effect if you do not use auto parallel.") return dict( cluster_key=device_name.replace(" ", "-"), - cluster_info=infer_cluster_info(), + cluster_info=cluster_info, ) diff --git a/tensorrt_llm/builder.py b/tensorrt_llm/builder.py index 7bc90a4f6..65390bcc7 100644 --- a/tensorrt_llm/builder.py +++ b/tensorrt_llm/builder.py @@ -18,7 +18,7 @@ import os import shutil import time -from dataclasses import dataclass +from dataclasses import dataclass, field from pathlib import Path from typing import Dict, Optional, Union @@ -458,10 +458,11 @@ class BuildConfig: input_timing_cache: str = None output_timing_cache: str = None lora_config: LoraConfig = LoraConfig() - auto_parallel_config: AutoParallelConfig = AutoParallelConfig() + auto_parallel_config: AutoParallelConfig = field( + default_factory=AutoParallelConfig) weight_sparsity: bool = False weight_streaming: bool = False - plugin_config: PluginConfig = PluginConfig() + plugin_config: PluginConfig = field(default_factory=PluginConfig) use_strip_plan: bool = False max_encoder_input_len: int = 1 # for enc-dec DecoderModel use_fused_mlp: bool = False @@ -486,6 +487,16 @@ def __post_init__(self): ) self.max_num_tokens, self.opt_num_tokens = max_num_tokens, opt_num_tokens + if self.plugin_config.remove_input_padding and self.plugin_config.context_fmha: + if self.max_input_len: + logger.warning( + 'padding removal and fMHA are both enabled, max_input_len is not required and will be ignored' + ) + else: + assert self.max_input_len is not None, 'padding removal and fMHA aren\'t both enabled, max_input_len is required' + if self.max_seq_len: + assert self.max_input_len <= self.max_seq_len, 'max_input_len should not be larger than max_seq_len' + @classmethod def from_dict(cls, config, plugin_config=None): max_input_len = config.pop('max_input_len') @@ -720,7 +731,9 @@ def optimize_model_with_config(model: PretrainedModel, return model -def build(model: PretrainedModel, build_config: BuildConfig) -> Engine: +def build(model: PretrainedModel, + build_config: BuildConfig, + return_build_config: bool = False) -> Engine | BuildConfig: '''Build engine from given model and optimization options specified in the build_config WARNING: this function may change the given \p model object state in some optimization passes to avoid cloning a model since normally the LLM models consumes large memory. @@ -820,6 +833,11 @@ def build(model: PretrainedModel, build_config: BuildConfig) -> Engine: network.plugin_config.set_nccl_plugin( nccl_plugin, network.plugin_config.use_custom_all_reduce) + # NOTE: Please never change the build_config object after this point! + if return_build_config: + # Get an modified build_config that is the same as the one in the final engine dir + return build_config + with net_guard(network): # Prepare network.set_named_parameters(model.named_parameters()) diff --git a/tensorrt_llm/commands/build.py b/tensorrt_llm/commands/build.py index 4384894f1..ef71cc6aa 100644 --- a/tensorrt_llm/commands/build.py +++ b/tensorrt_llm/commands/build.py @@ -20,7 +20,7 @@ from concurrent.futures import ProcessPoolExecutor, as_completed from importlib.machinery import SourceFileLoader from multiprocessing import get_context -from typing import Union +from typing import Optional, Union import torch @@ -82,8 +82,10 @@ def parse_arguments(): '--max_decoder_seq_len', dest='max_seq_len', type=int, - default=2048, - help="Max total length of context and generated sequence") + default=None, + help= + "Max total length of context and generated sequence. If unspecified, will try to deduce from the model config." + ) parser.add_argument('--max_output_len', type=int, default=None) parser.add_argument('--max_beam_width', type=int, default=1) parser.add_argument('--max_num_tokens', type=int, default=8192) @@ -253,12 +255,16 @@ def preprocess_model_config(model_config, **kwargs): model_config.mapping.world_size = kwargs['tp_size'] * kwargs['pp_size'] -def build_model(build_config: BuildConfig, - rank: int = 0, - ckpt_dir: str = None, - model_config: Union[str, PretrainedConfig] = None, - model_cls=None, - **kwargs) -> Engine: +def build_model( + build_config: BuildConfig, + rank: int = 0, + ckpt_dir: str = None, + model_config: Union[str, PretrainedConfig] = None, + model_cls=None, + dry_run: + bool = False, # return the modified BuildConfig without actually building the engine + **kwargs +) -> Union[Engine, BuildConfig]: model_config = copy.deepcopy(model_config) logits_dtype = kwargs.get('logits_dtype') @@ -314,6 +320,9 @@ def build_model(build_config: BuildConfig, build_config.use_strip_plan = True build_config.use_refit = kwargs.get('refit', False) + if dry_run: + return build_config + return build(model, build_config) @@ -332,22 +341,14 @@ def build_and_save(rank, gpu_id, ckpt_dir, build_config, output_dir, log_level, return True -def parallel_build(ckpt_dir_or_model_config: str, +def parallel_build(model_config: PretrainedConfig, + ckpt_dir: Optional[str], build_config: BuildConfig, output_dir: str, workers: int = 1, log_level: str = 'info', model_cls=None, **kwargs): - if ckpt_dir_or_model_config.lower().endswith('.json'): - config_path = ckpt_dir_or_model_config - ckpt_dir = None - else: - config_path = os.path.join(ckpt_dir_or_model_config, 'config.json') - ckpt_dir = ckpt_dir_or_model_config - - model_config = PretrainedConfig.from_json_file(config_path) - preprocess_model_config(model_config, **kwargs) if build_config.auto_parallel_config.enabled: if model_config.mapping.world_size > 1: @@ -417,6 +418,18 @@ def main(): 'refit': False, } speculative_decoding_mode = SpeculativeDecodingMode.from_arguments(args) + + ckpt_dir_or_model_config = args.checkpoint_dir if args.checkpoint_dir is not None else args.model_config + if ckpt_dir_or_model_config.lower().endswith('.json'): + config_path = ckpt_dir_or_model_config + ckpt_dir = None + else: + config_path = os.path.join(ckpt_dir_or_model_config, 'config.json') + ckpt_dir = ckpt_dir_or_model_config + + model_config = PretrainedConfig.from_json_file(config_path) + preprocess_model_config(model_config, **kwargs) + if args.build_config is None: if args.multiple_profiles == "enable" and args.opt_num_tokens is not None: raise RuntimeError( @@ -443,6 +456,38 @@ def main(): del args.max_output_len + # Extract rotary scaling which will be used for checks and default value of max_seq_len + rotary_scaling = getattr(model_config, "rotary_scaling", None) + if rotary_scaling is not None: + rotary_type = rotary_scaling['type'] + rotary_factor = rotary_scaling[ + 'factor'] if rotary_type != 'su' else 1 + else: + rotary_factor = 1 + + if args.max_seq_len is None: + # Step 1: Find the upper bound of max_seq_len + deduced_max_seq_len = 2048 + if model_config.max_position_embeddings is not None: + deduced_max_seq_len = model_config.max_position_embeddings + + # Step 2: Scale max_seq_len with rotary scaling + rotary_scaling = getattr(model_config, "rotary_scaling", None) + if rotary_factor != 1: + deduced_max_seq_len *= rotary_factor + logger.warning( + f'max_seq_len is scaled to {deduced_max_seq_len} by rotary scaling {rotary_factor}' + ) + + # Step 3: Assign the new max_seq_len + args.max_seq_len = deduced_max_seq_len + logger.info( + f'max_seq_len is not specified, using value {deduced_max_seq_len}' + ) + else: + if not plugin_config.streamingllm and model_config.max_position_embeddings is not None: + assert args.max_seq_len <= model_config.max_position_embeddings * rotary_factor, f'max_seq_len {args.max_seq_len} can\'t be larger than max_position_embeddings {model_config.max_position_embeddings} * rotary scaling {rotary_factor}' + build_config = BuildConfig.from_dict( { 'max_input_len': args.max_input_len, @@ -488,9 +533,8 @@ def main(): build_config = BuildConfig.from_json_file(args.build_config, plugin_config=plugin_config) - source = args.checkpoint_dir if args.checkpoint_dir is not None else args.model_config - parallel_build(source, build_config, args.output_dir, workers, - args.log_level, model_cls, **kwargs) + parallel_build(model_config, ckpt_dir, build_config, args.output_dir, + workers, args.log_level, model_cls, **kwargs) tok = time.time() t = time.strftime('%H:%M:%S', time.gmtime(tok - tik)) diff --git a/tensorrt_llm/executor.py b/tensorrt_llm/executor.py index 2aefb7714..1b26b128b 100644 --- a/tensorrt_llm/executor.py +++ b/tensorrt_llm/executor.py @@ -5,6 +5,7 @@ import threading import time from abc import ABC, abstractmethod +from dataclasses import dataclass, field from multiprocessing.connection import Client, Listener from pathlib import Path from queue import Queue @@ -14,23 +15,13 @@ import torch from janus import Queue as AsyncQueue -from tensorrt_llm._utils import mpi_rank, mpi_world_size -from tensorrt_llm.hlapi.mpi_session import (MpiPoolSession, MpiSession, - external_mpi_comm_available, - find_free_port, - need_spawn_mpi_workers) -from tensorrt_llm.hlapi.tokenizer import TokenizerBase, tokenizer_factory -from tensorrt_llm.hlapi.utils import (ContextManager, GenerationOutput, - print_traceback_on_error) - from ._utils import mpi_rank, mpi_world_size from .bindings import executor as tllm from .hlapi.mpi_session import (MpiPoolSession, MpiSession, external_mpi_comm_available, find_free_port, need_spawn_mpi_workers) -from .hlapi.tokenizer import TokenizerBase, tokenizer_factory -from .hlapi.utils import (ContextManager, GenerationOutput, SamplingParams, - exception_handler, print_traceback_on_error) +from .hlapi.utils import (ContextManager, SamplingParams, exception_handler, + print_traceback_on_error) def has_event_loop() -> bool: @@ -45,34 +36,21 @@ class GenerationRequest: def __init__( self, - ids_or_prompt: Union[torch.Tensor, np.ndarray, list, str], - streaming: bool = True, - tokenizer: Optional[TokenizerBase] = None, - sampling_params: Optional[Union[SamplingParams, - List[SamplingParams]]] = None, + prompt_token_ids: Union[torch.Tensor, np.ndarray, list], + sampling_params: SamplingParams, + streaming: bool = False, ): - if isinstance(ids_or_prompt, str): - assert tokenizer is not None, "GenerationRequest constructor with str prompt requires a tokenizer argument" - self.input_ids = (tokenizer.encode(ids_or_prompt, - return_tensors="pt", - return_attention_mask=False).to( - torch.int32).numpy()) + if isinstance(prompt_token_ids, list): + self.prompt_token_ids = prompt_token_ids + elif isinstance(prompt_token_ids, (torch.Tensor, np.ndarray)): + self.prompt_token_ids = prompt_token_ids.tolist() else: - if isinstance(ids_or_prompt, list): - self.input_ids = np.array(ids_or_prompt, dtype="int32") - elif isinstance(ids_or_prompt, torch.Tensor): - self.input_ids = ids_or_prompt.to(torch.int32).numpy() - elif isinstance(ids_or_prompt, np.ndarray): - self.input_ids = ids_or_prompt - else: - raise ValueError( - f"ids_or_prompt (={ids_or_prompt}) should be an instance of str, torch.Tensor, np.ndarray or list" - ) + raise TypeError( + f"prompt_token_ids ({prompt_token_ids}) should be an instance of torch.Tensor, np.ndarray or list" + ) - self.tokenizer = tokenizer + self.sampling_params = sampling_params self.streaming = streaming - self.sampling_params = sampling_params or SamplingParams() - self.id = -1 def set_id(self, id): @@ -80,27 +58,19 @@ def set_id(self, id): return self def as_executor_request(self) -> tllm.Request: - # Request - # TODO: Should we unify the pad_id/end_id logic? - end_id = self.tokenizer.eos_token_id if self.tokenizer is not None else None - pad_id = self.tokenizer.pad_token_id if self.tokenizer is not None else None - if end_id is None: - end_id = self.sampling_params.end_id - pad_id = end_id if pad_id is None else pad_id - request_kwargs = { "input_token_ids": - self.input_ids.squeeze().tolist(), + self.prompt_token_ids, "max_new_tokens": - self.sampling_params.max_new_tokens or 32, + self.sampling_params.max_new_tokens, "streaming": self.streaming, "sampling_config": self.sampling_params._get_sampling_config(), "end_id": - end_id, + self.sampling_params.end_id, "pad_id": - pad_id, + self.sampling_params.pad_id, "output_config": self.sampling_params._get_output_config(), # The following options in the Executor API are not yet exposed by the HLAPI: @@ -124,16 +94,43 @@ def as_executor_request(self) -> tllm.Request: return request -class GenerationResult(GenerationOutput): +@dataclass(slots=True) +class CompletionOutput: + """The output data of one completion output of a request. + + Args: + index (int): The index of the output in the request. + text (str): The generated output text. + token_ids (List[int]): The token ids of the generated output text. + cumulative_logprob (float): The cumulative log probability of the generated output text. + logprobs (List[float]): The log probabilities of the top probability words at each position if the logprobs are requested. + generation_logits (torch.Tensor): The logits on the generated output token ids. + """ + index: int + text: str = "" + token_ids: List[int] = field(default_factory=list) + cumulative_logprob: Optional[float] = None + logprobs: List[float] = field(default_factory=list) + generation_logits: Optional[torch.Tensor] = field(default=None, repr=False) + _last_text: str = field(default="", init=False, repr=False) + + @property + def length(self): + return len(self.token_ids) - def __init__(self, - generation_request: GenerationRequest, - tokenizer: Optional[TokenizerBase] = None) -> None: + @property + def text_diff(self) -> str: + diff = self.text[len(self._last_text):] + self._last_text = self.text + return diff + + +class GenerationResult: + + def __init__(self, generation_request: GenerationRequest) -> None: self._done = False self._cancelled = False - self.generation_request = generation_request - self.tokenizer = tokenizer - self.streaming = generation_request.streaming + self._generation_request = generation_request if has_event_loop(): aqueue = AsyncQueue() @@ -143,32 +140,50 @@ def __init__(self, self.queue = Queue() self.aqueue = None - beam_width = generation_request.sampling_params.beam_width + self.outputs: List[CompletionOutput] = [ + CompletionOutput(i) for i in range(self.beam_width) + ] + self.context_logits: Optional[torch.Tensor] = None + + @property + def request_id(self) -> int: + return self._generation_request.id + + @property + def prompt_token_ids(self) -> List[int]: + return self._generation_request.prompt_token_ids - self.beam_search_enabled = beam_width > 1 - self._token_ids = [[] for _ in range(beam_width)] + @property + def finished(self) -> bool: + return self._done - self.logprobs = [] - self.last_text = "" + @property + def streaming(self): + return self._generation_request.streaming @property - def token_ids(self): - if not self.beam_search_enabled: - return self._token_ids[0] - return self._token_ids + def beam_width(self): + return self._generation_request.sampling_params.beam_width def handle_generation_msg(self, tensors: tuple, error: str): if error: raise RuntimeError(error) - output_token_ids, context_logits, generation_logits, log_probs = tensors + output_token_ids, context_logits, generation_logits, log_probs, cum_log_probs = tensors - for idx, beam_ids in enumerate(output_token_ids): - self._token_ids[idx] += beam_ids + for i, beam_ids in enumerate(output_token_ids): + self.outputs[i].token_ids.extend(beam_ids) + if cum_log_probs is not None: + self.outputs[i].cumulative_logprob = cum_log_probs[i] + if log_probs is not None: + self.outputs[i].logprobs = log_probs[i] + assert len(self.outputs[i].logprobs) == self.outputs[i].length + if generation_logits is not None: + self.outputs[i].generation_logits = generation_logits[ + i, :self.outputs[i].length] - self.context_logits = context_logits - self.generation_logits = generation_logits - self.log_probs = log_probs + if context_logits is not None: + self.context_logits = context_logits def result_step(self, timeout: Optional[float] = None): _, tensors, self._done, error = self.queue.get(timeout=timeout) @@ -179,25 +194,6 @@ async def aresult_step(self): _, tensors, self._done, error = await self.aqueue.get() self.handle_generation_msg(tensors, error) - @property - def text_diff(self) -> str: - assert self.streaming is not None - assert not self.beam_search_enabled, "text_diff is not supported with beam_search" - - new_txt = self.text - diff = new_txt[len(self.last_text):] - self.last_text = new_txt - return diff - - @property - def text(self) -> Union[str, List[str]]: - if self.tokenizer is None: - return '' - texts = self.tokenizer.batch_decode(self._token_ids) - if not self.beam_search_enabled: - return texts[0] - return texts - def result(self, timeout: Optional[float] = None) -> "GenerationResult": while not self._done: self.result_step(timeout) @@ -208,6 +204,9 @@ async def aresult(self) -> "GenerationResult": await self.aresult_step() return self + def __await__(self): + return self.aresult().__await__() + def __iter__(self): return self @@ -246,13 +245,27 @@ def exception(self, timeout: Optional[float] = None): except RuntimeError as e: return e + def _repr_fields(self): + return ['request_id', 'prompt_token_ids', 'outputs', 'finished'] + + def __repr__(self) -> str: + repr = [] + for field in self._repr_fields(): + value = getattr(self, field) + if isinstance(value, str): + repr.append(f"{field}={value!r}") + else: + repr.append(f"{field}={value}") + repr = ", ".join(repr) + repr = f"{self.__class__.__name__}({repr})" + return repr + class GenerationExecutor(ABC): TERMINATE_REQUEST_ID = 0 def __init__(self): self.id_counter = GenerationExecutor.TERMINATE_REQUEST_ID + 1 - self.tokenizer = None self._stats = None self.stats_queue = None @@ -277,52 +290,48 @@ def submit(self, request: GenerationRequest) -> GenerationResult: def generate_async( self, - prompt: Union[str, List[int], List[str], List[List[int]]], - streaming: bool, - sampling_params: Optional[Union[SamplingParams, - List[SamplingParams]]] = None, + prompt_token_ids: List[int], + sampling_params: SamplingParams, + streaming: bool = False, + ) -> GenerationResult: + """Generate output for the given prompt token ids in the asynchronous mode. + Asynchronous generation accepts single prompt only. + """ + assert isinstance(prompt_token_ids[0], int) + assert isinstance(sampling_params, SamplingParams) + result = self.submit( + GenerationRequest(prompt_token_ids, + sampling_params=sampling_params, + streaming=streaming)) + return result + + def generate( + self, prompt_token_ids: Union[List[int], List[List[int]]], + sampling_params: Union[SamplingParams, List[SamplingParams]] ) -> Union[GenerationResult, List[GenerationResult]]: - unbatched = isinstance(prompt, str) or (isinstance(prompt, list) - and isinstance(prompt[0], int)) - string_input = isinstance( - prompt, str) or (not unbatched and isinstance(prompt[0], str)) - tokenizer = self.tokenizer if string_input else None + """Generate output for the given prompt token ids in the synchronous mode. + Synchronous generation accepts either single prompt or batched prompts. + """ + unbatched = isinstance(prompt_token_ids[0], int) if unbatched: - results = self.submit( - GenerationRequest(prompt, - streaming, - tokenizer, - sampling_params=sampling_params)) - else: - sampling_params = [sampling_params] * len(prompt) if not isinstance( - sampling_params, list) else sampling_params - results = [] - for idx, p in enumerate(prompt): - results.append( - self.submit( - GenerationRequest( - p, - streaming, - tokenizer, - sampling_params=sampling_params[idx]))) - return results + prompt_token_ids = [prompt_token_ids] + + futures = [] + for i, p in enumerate(prompt_token_ids): + if isinstance(sampling_params, list): + sp = sampling_params[i] + else: + sp = sampling_params + future = self.generate_async(p, sampling_params=sp, streaming=False) + futures.append(future) + + for future in futures: + future.result() + + if unbatched: + futures = futures[0] - def generate( - self, - prompt: Union[str, List[int], List[str], List[List[int]]], - streaming: bool = False, - sampling_params: Optional[Union[SamplingParams, - List[SamplingParams]]] = None, - ) -> Union[GenerationResult, List[GenerationResult]]: - futures = self.generate_async(prompt, - streaming=streaming, - sampling_params=sampling_params) - if isinstance(futures, GenerationRequest): - futures.result() - else: - for future in futures: - future.result() return futures @abstractmethod @@ -351,7 +360,6 @@ async def aget_stats(self): @staticmethod def create( engine_dir: Path, - tokenizer: Union[str, Path, TokenizerBase], executor_config: tllm.ExecutorConfig = tllm.ExecutorConfig(1), model_world_size: int = 1, world_size: int = 0, @@ -370,7 +378,6 @@ def create( worker_kwargs = { "engine_dir": engine_dir, - "tokenizer": tokenizer, "executor_config": executor_config, } @@ -396,13 +403,11 @@ class WorkerExit(GeneratorExit): def __init__( self, engine_dir: Path, - tokenizer: Union[str, Path, TokenizerBase, None], executor_config: tllm.ExecutorConfig = tllm.ExecutorConfig(1), ) -> None: super().__init__() self.engine = None - self.tokenizer = tokenizer_factory(tokenizer) self._results: Dict[int, GenerationResult] = {} self._pending: set = set() self.result_queue = None @@ -475,6 +480,7 @@ def awaiter_loop(self): response.result.context_logits, response.result.generation_logits, response.result.log_probs, + response.result.cum_log_probs, ) self.return_queue(req_id).put( (response.request_id, tensors, response.result.is_final, @@ -492,19 +498,23 @@ def stats_loop(self): self.stats_queue.get() self.stats_queue.put(stats.to_json_str()) + def start(self): + self.create_stats_queue() + self.start_awaiter_thread() + self.start_stats_thread() + def submit(self, request: GenerationRequest) -> GenerationResult: """ Low-level API to the executor. Return a "future" GenerationResult which can be waited. """ + self.start() + if self.rank != 0: raise NotImplementedError("Only rank 0 can submit requests.") - self.create_stats_queue() - self.start_awaiter_thread() - self.start_stats_thread() req_id = self.engine.enqueue_request(request.as_executor_request()) request.set_id(req_id) - result = GenerationResult(request, request.tokenizer) + result = GenerationResult(request) self._results[req_id] = result self._pending.add(req_id) return result @@ -540,12 +550,12 @@ def __del__(self): def wait_first_completed( self, futures: List[GenerationResult] ) -> Generator[GenerationResult, None, None]: - wait_set = set(f.generation_request.id for f in futures) + wait_set = set(f.request_id for f in futures) # clear already-finished requests for f in futures: if f._done: - wait_set.remove(f.generation_request.id) + wait_set.remove(f.request_id) yield f # wait remaining active requests @@ -597,7 +607,6 @@ def __init__( super().__init__() self.workers_started = False - self.tokenizer = tokenizer_factory(workers_kwargs["tokenizer"]) request_queue_addr = ("127.0.0.1", find_free_port(), secrets.token_bytes(512)) @@ -642,7 +651,6 @@ def __init__( @staticmethod def workers_main( engine_dir: Path, - tokenizer: Union[str, Path, TokenizerBase], request_queue_addr: Tuple[str, int, bytes], request_id_queue_addr: Tuple[str, int, bytes], result_queue_addr: Tuple[str, int, bytes], @@ -663,8 +671,7 @@ def workers_main( # TODO[chunweiy]: fix the non-rank0 process failure init_ok = True try: - executor = ExecutorBindingsWorker(engine_dir, tokenizer, - executor_config) + executor = ExecutorBindingsWorker(engine_dir, executor_config) except Exception as e: init_ok = False raise e @@ -678,7 +685,7 @@ def workers_main( executor.set_stats_queue(mp_stats_queue) while (req := request_queue.get()) is not None: result = executor.submit(req) - request_id_queue.put(result.generation_request.id) + request_id_queue.put(result.request_id) result_queue.put(None) mp_stats_queue.put(None) @@ -738,20 +745,14 @@ def submit(self, request: GenerationRequest) -> GenerationResult: if not self.workers_started: self.start() - tokenizer = request.tokenizer - # no need to send the tokenizer to the executor, - # saves communication time - request.tokenizer = None - self.request_queue.put(request) # Await req id. req_id = self.request_id_queue.get() request.set_id(req_id) - result = GenerationResult(request, tokenizer) + result = GenerationResult(request) self._results[req_id] = result - request.tokenizer = tokenizer self._request_id_dispatcher_queue.put(req_id) return result diff --git a/tensorrt_llm/functional.py b/tensorrt_llm/functional.py index 037e52f10..08e768d9c 100644 --- a/tensorrt_llm/functional.py +++ b/tensorrt_llm/functional.py @@ -4072,11 +4072,6 @@ def bert_attention(tensor: Tensor, q_scaling = trt.PluginField("q_scaling", np.array(q_scaling, dtype=np.float32), trt.PluginFieldType.FLOAT32) - enable_qk_half_accum = trt.PluginField( - "enable_qk_half_accum", - np.array(np.int8( - default_net().plugin_config.attention_qk_half_accumulation), - dtype=np.int8), trt.PluginFieldType.INT8) context_fmha_type = trt.PluginField( "context_fmha_type", np.array(np.int8(default_net().plugin_config.context_fmha_type), @@ -4097,8 +4092,8 @@ def bert_attention(tensor: Tensor, np.array(np.int8(default_net().plugin_config.remove_input_padding), dtype=np.int8), trt.PluginFieldType.INT8) pfc = trt.PluginFieldCollection([ - nheads, head_size, q_scaling, enable_qk_half_accum, context_fmha_type, - pf_type, do_relative_attention, max_distance, remove_padding + nheads, head_size, q_scaling, context_fmha_type, pf_type, + do_relative_attention, max_distance, remove_padding ]) attn_plug = attn_plg_creator.create_plugin("padding_attn", pfc) @@ -6008,3 +6003,26 @@ def topk(input: Tensor, indices = layer.get_output(1) return _create_tensor(values, layer), _create_tensor(indices, layer) + + +def scatter_nd(input: Tensor, mask: Tensor, source: Tensor) -> Tensor: + ''' + Scatter_nd is a tensor operation that writes or updates values in a tensor based on indices. + + Parameters: + input: Tensor + The input tensor to be updated + mask: Tensor + A tensor of indices specifying the locations in data to be updated. + source: Tensor + A tensor of values to be written or scattered into data. + Returns: + New tensor with the same shape as the input tensor data, + where the values from the source tensor are scattered or written into the output tensor + at the locations specified by the mask tensor. + ''' + scatter_layer = default_trtnet().add_scatter(input.trt_tensor, + mask.trt_tensor, + source.trt_tensor, + mode=trt.ScatterMode.ND) + return _create_tensor(scatter_layer.get_output(0), scatter_layer) diff --git a/tensorrt_llm/hlapi/__init__.py b/tensorrt_llm/hlapi/__init__.py index 8e17111ef..9ca9fc031 100644 --- a/tensorrt_llm/hlapi/__init__.py +++ b/tensorrt_llm/hlapi/__init__.py @@ -1,8 +1,17 @@ -from .llm import (LLM, CapacitySchedulerPolicy, KvCacheConfig, ModelConfig, - ParallelConfig, SamplingParams, StreamingLLMParam) +from .llm import LLM, SamplingParams +from .llm_utils import (BuildConfig, CapacitySchedulerPolicy, KvCacheConfig, + LlmArgs, QuantAlgo, QuantConfig, SchedulerConfig) from .tokenizer import TokenizerBase __all__ = [ - 'LLM', 'ModelConfig', 'TokenizerBase', 'SamplingParams', 'ParallelConfig', - 'StreamingLLMParam', 'KvCacheConfig', 'CapacitySchedulerPolicy' + 'LLM', + 'TokenizerBase', + 'SamplingParams', + 'KvCacheConfig', + 'SchedulerConfig', + 'CapacitySchedulerPolicy', + 'BuildConfig', + 'QuantConfig', + 'QuantAlgo', + 'LlmArgs', ] diff --git a/tensorrt_llm/hlapi/_perf_evaluator.py b/tensorrt_llm/hlapi/_perf_evaluator.py index 41347b8e5..027e35570 100644 --- a/tensorrt_llm/hlapi/_perf_evaluator.py +++ b/tensorrt_llm/hlapi/_perf_evaluator.py @@ -15,7 +15,7 @@ from .._utils import release_gc from ..profiler import device_memory_info, host_memory_info -from .llm import LLM, ModelConfig, SamplingParams +from .llm import LLM, SamplingParams from .utils import is_directory_empty, print_colored @@ -37,7 +37,7 @@ class Report: avg_latency: float seq_throughput: float token_throughput: float - ave_tokens_per_sample: int + ave_tokens_per_sample: float memory_usage_samples: "MemoryContinuousMonitorThread.RecordList" = field( default_factory=list) @@ -149,28 +149,25 @@ def get_gpu_memory_usage() -> Iterable[float]: class LLMPerfEvaluator: @classmethod - def create( - cls, - model_config: ModelConfig, - samples_path: Path, - num_samples: int = -1, - warmup: int = 100, - batch_size: int = -1, - engine_cache_path: Optional[Path] = None, - memory_monitor_interval: Optional[int] = None, - # The additional arguments are for the LLM constructor - **kwargs - ) -> Optional['LLMPerfEvaluator']: + def create(cls, + model: str, + samples_path: Path, + num_samples: int = -1, + warmup: int = 100, + batch_size: int = -1, + engine_cache_path: Optional[Path] = None, + memory_monitor_interval: Optional[int] = None, + **kwargs) -> 'LLMPerfEvaluator': ''' Args: - model_config: ModelConfig + model: The model name or a local path to the model directory. samples_path: path to the input data samples - kv_cache_free_gpu_memory_fraction: the fraction of free GPU memory to use for the key-value cache num_samples: number of the heading samples to run, if set to -1, all samples will be used warmup: number of samples for warmup runs batch_size: batch size for the runs, if left default, the batch size will be the same as the number of samples engine_cache_path: path to the engine file, if provided, the engine will save the built engine to the path and reuse it for the next runs memory_monitor_interval: the interval to monitor the host and GPU memory usage, if set to None, the memory monitor will be disabled + kwargs: the additional arguments are for the LLM constructor ''' from_cache = False @@ -179,7 +176,7 @@ def create( ) and not is_directory_empty(engine_cache_path): print(f"Loading engine from {engine_cache_path}\n") from_cache = True - model_config.model_dir = engine_cache_path + model = engine_cache_path memory_monitor_thread = None if memory_monitor_interval is not None: @@ -189,10 +186,9 @@ def create( # TODO[chunweiy]: Fixit, this barely work, the cpp runtime will trigger RuntimeError, which cannot be caught try: - llm = LLM(model_config, **kwargs) + llm = LLM(model, skip_tokenizer_init=True, **kwargs) except Exception as e: - logger.error( - f"Failed to create LLM with {model_config} and {kwargs}") + logger.error(f"Failed to create LLM with {model} and {kwargs}") raise e if engine_cache_path is not None and not from_cache: @@ -213,7 +209,7 @@ def create( memory_monitor_thread=memory_monitor_thread) def __init__(self, - llm, + llm: LLM, samples: List[Sample], warmup: int, max_num_samples: int, @@ -256,16 +252,15 @@ async def lane(num_tasks: int, start = time.time() output = self.llm.generate_async( - sample.input_ids, - sampling_params=SamplingParams( - max_new_tokens=sample.output_len)) + sample.input_ids, sampling_params=sampling_params) output = await output.aresult() end = time.time() perf_item = PerfItem(start=start, end=end, - num_out_tokens=len(output.token_ids) - - len(sample.input_ids)) + num_out_tokens=sum( + beam_output.length + for beam_output in output.outputs)) if not warmup: self.perf_items.append(perf_item) diff --git a/tensorrt_llm/hlapi/build_cache.py b/tensorrt_llm/hlapi/build_cache.py new file mode 100644 index 000000000..6fcb6cf29 --- /dev/null +++ b/tensorrt_llm/hlapi/build_cache.py @@ -0,0 +1,272 @@ +import contextlib +import datetime +import enum +import hashlib +import json +import os +import shutil +from dataclasses import dataclass +from pathlib import Path +from typing import Any, List, Optional + +import filelock + +import tensorrt_llm +from tensorrt_llm.hlapi.llm_utils import BuildConfig +from tensorrt_llm.logger import logger + + +def get_build_cache_config_from_env() -> tuple[bool, str]: + """ + Get the build cache configuration from the environment variables + """ + build_cache_enabled = os.environ.get('TLLM_HLAPI_BUILD_CACHE') == '1' + build_cache_root = os.environ.get( + 'TLLM_HLAPI_BUILD_CACHE_ROOT', + '/tmp/.cache/tensorrt_llm/hlapi/') # nosec B108 + return build_cache_enabled, build_cache_root + + +class BuildCache: + ''' + The BuildCache class is a class that manages the intermediate products from the build steps. + + NOTE: currently, only engine-building is supported + TODO[chunweiy]: add support for other build steps, such as quantization, convert_checkpoint, etc. + ''' + # The version of the cache, will be used to determine if the cache is compatible + CACHE_VERSION = 0 + + def __init__(self, + cache_root: Optional[Path] = None, + max_records: int = 10, + max_cache_storage_gb: int = 256): + ''' + Args: + cache_root (Path): The root directory of the cache + max_records (int): The maximum number of records to keep in the cache + max_cache_storage_gb (int): The maximum storage size of the cache + ''' + _, default_cache_root = get_build_cache_config_from_env() + self.cache_root = cache_root or Path(default_cache_root) + self.max_records = max_records + self.max_cache_storage_gb = max_cache_storage_gb + + if max_records < 1: + raise ValueError("max_records should be greater than 0") + + def get_engine_building_cache_stage(self, + build_config: BuildConfig, + model_path: Optional[Path] = None, + **kwargs) -> 'CachedStage': + ''' + Get the build step for engine building. + ''' + from tensorrt_llm.hlapi.llm_utils import \ + _ModelFormatKind # avoid cyclic import + force_rebuild = False + if parallel_config := kwargs.get('parallel_config'): + if parallel_config.auto_parallel: + force_rebuild = True + if model_format := kwargs.get('model_format'): + if model_format is not _ModelFormatKind.HF: + force_rebuild = True + + build_config_str = BuildCache.prune_build_config_for_cache_key( + build_config.to_dict()) + + return CachedStage(parent=self, + kind=CacheRecord.Kind.Engine, + cache_root=self.cache_root, + force_rebuild=force_rebuild, + inputs=[build_config_str, model_path, kwargs]) + + def prune_caches(self, has_incoming_record: bool = False): + ''' + Clean up the cache records to make sure the cache size is within the limit + + Args: + has_incoming_record (bool): If the cache has incoming record, the existing records will be further pruned to + reserve space for the incoming record + ''' + if not self.cache_root.exists(): + return + self._clean_up_cache_dir() + records = [] + for dir in self.cache_root.iterdir(): + records.append(self._load_cache_record(dir)) + records.sort(key=lambda x: x.time, reverse=True) + max_records = self.max_records - 1 if has_incoming_record else self.max_records + # prune the cache to meet max_records and max_cache_storage_gb limitation + while len(records) > max_records or sum( + r.storage_gb for r in records) > self.max_cache_storage_gb: + record = records.pop() + # remove the directory and its content + shutil.rmtree(record.path) + + @staticmethod + def prune_build_config_for_cache_key(build_config: dict) -> dict: + # The BuildCache will be disabled once auto_pp is enabled, so 'auto_parallel_config' should be removed + black_list = ['auto_parallel_config', 'dry_run'] + dic = build_config.copy() + for key in black_list: + if key in dic: + dic.pop(key) + return dic + + def load_cache_records(self) -> List["CacheRecord"]: + ''' + Load all the cache records from the cache directory + ''' + records = [] + if not self.cache_root.exists(): + return records + + for dir in self.cache_root.iterdir(): + records.append(self._load_cache_record(dir)) + return records + + def _load_cache_record(self, cache_dir) -> "CacheRecord": + ''' + Get the cache record from the cache directory + ''' + metadata = json.loads((cache_dir / 'metadata.json').read_text()) + storage_gb = sum(f.stat().st_size for f in cache_dir.glob('**/*') + if f.is_file()) / 1024**3 + return CacheRecord(kind=CacheRecord.Kind.__members__[metadata['kind']], + storage_gb=storage_gb, + path=cache_dir, + time=datetime.datetime.fromisoformat( + metadata['datetime'])) + + def _clean_up_cache_dir(self): + ''' + Clean up the files in the cache directory, remove anything that is not in the cache + ''' + # get all the files and directies in the cache_root + if not self.cache_root.exists(): + return + for file_or_dir in self.cache_root.iterdir(): + if not self.is_cache_valid(file_or_dir): + logger.info(f"Removing invalid cache directory {dir}") + if file_or_dir.is_file(): + file_or_dir.unlink() + else: + shutil.rmtree(file_or_dir) + + def is_cache_valid(self, cache_dir: Path) -> bool: + ''' + Check if the cache directory is valid + ''' + if not cache_dir.exists(): + return False + + metadata_path = cache_dir / 'metadata.json' + if not metadata_path.exists(): + return False + + metadata = json.loads(metadata_path.read_text()) + if metadata.get('version') != BuildCache.CACHE_VERSION: + return False + + content = cache_dir / 'content' + if not content.exists(): + return False + + return True + + +@dataclass +class CachedStage: + ''' + CachedStage is a class that represents a stage in the build process, it helps to manage the intermediate product. + + The cache is organized as follows: + + this_cache_dir/ # name is like "engine-" + metadata.json # the metadata of the cache + content/ # the actual product of the build step, such trt-llm engine directory + ''' + # The parent should be kept alive by CachedStep instance + parent: BuildCache + cache_root: Path + # The inputs will be used to determine if the step needs to be re-run, so all the variables should be put here + inputs: List[Any] + kind: "CacheRecord.Kind" + # If force_rebuild is set to True, the cache will be ignored + force_rebuild: bool = False + + def get_hash_key(self): + lib_version = tensorrt_llm.__version__ + input_strs = [str(i) for i in self.inputs] + return hashlib.md5( + f"{lib_version}-{input_strs}".encode()).hexdigest() # nosec B324 + + def get_cache_path(self) -> Path: + ''' + The path to the product of the build step, will be overwritten if the step is re-run + ''' + return self.cache_root / f"{self.kind.value}-{self.get_hash_key()}" + + def get_engine_path(self) -> Path: + return self.get_cache_path() / 'content' + + def get_cache_metadata(self) -> dict: + res = { + "version": BuildCache.CACHE_VERSION, + "datetime": datetime.datetime.now().isoformat(), + "kind": self.kind.name, + } + return res + + def cache_hitted(self) -> bool: + ''' + Check if the product of the build step is in the cache + ''' + if self.force_rebuild: + return False + try: + if self.get_cache_path().exists(): + metadata = json.loads( + (self.get_cache_path() / 'metadata.json').read_text()) + if metadata["version"] == BuildCache.CACHE_VERSION: + return True + except: + pass + + return False + + @contextlib.contextmanager + def write_guard(self): + ''' + Write the filelock to indicate that the build step is in progress + ''' + self.parent.prune_caches(has_incoming_record=True) + + target_dir = self.get_cache_path() + target_dir.mkdir(parents=True, exist_ok=True) + # TODO[chunweiy]: deal with the cache modification conflict + lock = filelock.FileLock(target_dir / '.filelock', timeout=10) + + with open(target_dir / 'metadata.json', 'w') as f: + f.write(json.dumps(self.get_cache_metadata())) + + lock.__enter__() + yield target_dir / 'content' + lock.__exit__(None, None, None) + + +@dataclass(unsafe_hash=True) +class CacheRecord: + ''' + CacheRecord is a class that represents a record in the cache directory. + ''' + + class Kind(enum.Enum): + Engine = 'engine' + Checkpoint = 'checkpoint' + + kind: Kind + storage_gb: float + path: Path + time: datetime.datetime diff --git a/tensorrt_llm/hlapi/llm.py b/tensorrt_llm/hlapi/llm.py index 0b2179d0c..dbba844b9 100644 --- a/tensorrt_llm/hlapi/llm.py +++ b/tensorrt_llm/hlapi/llm.py @@ -1,493 +1,222 @@ -import json import os import shutil import tempfile -import time -from argparse import Namespace -from dataclasses import dataclass, field -from enum import Enum from pathlib import Path -from typing import Any, Callable, Dict, Iterable, List, Optional, Union +from typing import Any, Iterable, List, Optional, Union -import tensorrt as trt -import torch - -from .._utils import mpi_barrier, mpi_rank, release_gc -from ..auto_parallel import AutoParallelConfig, infer_cluster_config +from .. import bindings as tllm from ..bindings import executor as tllm -from ..bindings.executor import (CapacitySchedulerPolicy, DecodingConfig, - KvCacheConfig) -from ..builder import BuildConfig, Engine, EngineConfig, build from ..executor import GenerationExecutor, GenerationResult from ..logger import logger -from ..mapping import Mapping -from ..models import MODEL_MAP -from ..models.modeling_utils import PretrainedConfig, QuantAlgo, QuantConfig -from ..module import Module -from .mpi_session import (MpiCommSession, MPINodeState, MpiPoolSession, - MpiSession, external_mpi_comm_available) -from .tokenizer import TokenizerBase, TransformersTokenizer -from .utils import (GenerationOutput, GpuArch, SamplingParams, - download_hf_model, exception_handler, file_with_glob_exists, - file_with_suffix_exists, get_device_count, init_log_level, - print_colored, print_traceback_on_error) - -init_log_level( -) # This should be called before importing the following cpp-runtime modules +from .llm_utils import (CachedModelLoader, LlmArgs, LlmBuildStats, ModelLoader, + _ModelRuntimeContext) +from .mpi_session import (MpiCommSession, MpiPoolSession, MpiSession, + external_mpi_comm_available) +from .tokenizer import TokenizerBase +# TODO[chunweiy]: move the following symbols back to utils scope, and remove the following import +from .utils import (SamplingParams, exception_handler, get_device_count, + init_log_level) + +# This should be called before importing the following cpp-runtime modules +init_log_level() -from ..builder import BuildConfig, Engine, EngineConfig, build from ..executor import GenerationExecutor, GenerationResult -@dataclass -class ParallelConfig: - ''' The model distribution configs for LLM. ''' - tp_size: int = 1 - pp_size: int = 1 - auto_parallel: bool = False - _world_size: int = field(default=1, init=False) - _devices: Optional[List[int]] = field(default=None, init=False) - - @property - def devices(self) -> List[int]: - if self._devices is None: - return list(range(self.world_size)) - return self._devices - - @devices.setter - def devices(self, devices: List[int]): - if len(devices) != self.world_size: - raise ValueError( - f"devices {devices} should have the same length as world_size {self.world_size}" - ) - self._devices = devices - - @property - def world_size(self) -> bool: - if self.auto_parallel: - if self.tp_size > 1 or self.pp_size > 1: - raise RuntimeError( - "manually TP and PP are not supported in auto parallel mode." - ) - return self._world_size - - if self._world_size > 1: - raise RuntimeError( - "world_size > 1 is only supported in auto parallel mode.") - return self.tp_size * self.pp_size - - @world_size.setter - def world_size(self, world_size: int): - if self.auto_parallel: - self._world_size = world_size - elif (not self.auto_parallel - ) and world_size != self.tp_size * self.pp_size: - raise ValueError( - f"world_size {world_size} should be equal to tp_size * pp_size {self.tp_size * self.pp_size} in non-auto_parallel mode.\n" - "For non-auto-parallel mode, the world_size is not needed to set" - ) - - @property - def is_multi_gpu(self) -> bool: - return self.world_size > 1 +class RequestOutput(GenerationResult): + """The output data of a completion request to the LLM. + Fields: + request_id (int): The unique ID of the request. + prompt (str): The prompt string of the request. + prompt_token_ids (List[int]): The token ids of the prompt. + outputs (List[CompletionOutput]): The output sequences of the request. + context_logits (torch.Tensor): The logits on the prompt token ids. + finished (bool): Whether the whole request is finished. + """ -@dataclass -class ModelConfig: - - # ``model_dir`` helps to locate a local model, the format of the model is determined by the model file itself. - # Either HF model, TensorRT-LLM checkpoints or TensorRT-LLM engine format is supported. - model_dir: Optional[str] = None - - # ``model`` could either be the model directory or a in-memory model. - # ``model`` specifies the model kind like "llama-7B", etc. - model: Optional[Union[str, Module]] = None - - # ``parallel_config`` is used to specify the parallelism of the model. - parallel_config: ParallelConfig = field(default_factory=ParallelConfig) - - # ``quant_config`` is used to specify the quantization mode of the model. - quant_config: QuantConfig = field(default_factory=QuantConfig) - - # ``build_config`` is used to specify the build options of the model. - build_config: BuildConfig = field( - default_factory=lambda: BuildConfig(max_num_tokens=1024), - init=False, - repr=False) - - def __post_init__(self): - if not (self.model_dir or self.model): - raise ValueError("Either model_dir or model should be provided.") - if self.model_dir and self.model: - raise ValueError( - "Only one of model_dir or model should be provided, provided both." - ) - - self._engine_config: Optional[EngineConfig] = None - - self.auto_parallel_config = AutoParallelConfig( - sharded_io_allowlist=[ - "past_key_value_\\d+", - "present_key_value_\\d*", - ], - same_buffer_io={ - "past_key_value_(\\d+)": "present_key_value_\\1", - }, - **infer_cluster_config(), - ) - - if self.model_dir: - model_path = Path(self.model_dir) - if not model_path.exists(): - raise ValueError( - f"model_dir of path {self.model_dir} does not exist.") - - # Load parallel_config from the engine. - self.model_format = ModelLoader.get_model_format(self.model_dir) - if self.model_format is _ModelFormatKind.TLLM_ENGINE: - self._load_config_from_engine(Path(self.model_dir)) - - # Load parallel_config from the checkpoint. - if self.model_format is _ModelFormatKind.TLLM_CKPT: - self._load_config_from_ckpt(Path(self.model_dir)) - else: - self.model_format = _ModelFormatKind.HF - - def _update_plugin_config(self, key: str, value: Any): - if key == 'use_paged_context_fmha': - self._validate_gpu_for_paged_context(value) - - setattr(self.build_config.plugin_config, key, value) - - def _validate_gpu_for_paged_context(self, value: bool): - if value: - devices = self.parallel_config.devices - if torch.cuda.get_device_properties(devices[0]).major < 8: - raise ValueError( - "Paged context is only supported on post Volta GPUs") - - def _load_config_from_engine(self, engine_dir: Path): - with open(engine_dir / "config.json") as f: - engine_config = json.load(f) - for config_key in ("pretrained_config", "build_config"): - if config_key not in engine_config: - raise ValueError( - f"Invalid engine config found from {engine_dir}, " - "please use the corresponding version of trtllm-build to build the engine." - ) - - pretrained_config = PretrainedConfig.from_dict( - engine_config["pretrained_config"]) - self.build_config = BuildConfig.from_dict( - engine_config["build_config"]) - - # load parallel_config - mapping = pretrained_config.mapping - if self.parallel_config.tp_size not in (1, mapping.tp_size): - raise ValueError( - f"tp_size {self.parallel_config.tp_size} is not consistent with the engine's tp_size {mapping.tp_size}" - ) - if self.parallel_config.pp_size not in (1, mapping.pp_size): - raise ValueError( - f"pp_size {self.parallel_config.pp_size} is not consistent with the engine's pp_size {mapping.pp_size}" - ) - self.parallel_config = ParallelConfig( - tp_size=mapping.tp_size, - pp_size=mapping.pp_size, - ) - - self._pretrined_config = pretrained_config + def __init__(self, + generation_result: GenerationResult, + prompt: Optional[str] = None, + tokenizer: Optional[TokenizerBase] = None) -> None: + self.__dict__.update(generation_result.__dict__) + self.prompt = prompt + self.tokenizer = tokenizer - def _load_config_from_ckpt(self, ckpt_dir: Path): - with open(ckpt_dir / "config.json") as f: - ckpt_config = json.load(f) - tp_size = ckpt_config["mapping"]["tp_size"] - pp_size = ckpt_config["mapping"]["pp_size"] - world_size = ckpt_config["mapping"]["world_size"] - # load parallel_config - if self.parallel_config.tp_size != 1 and self.parallel_config.tp_size != tp_size: - raise ValueError( - f"tp_size {self.parallel_config.tp_size} is not consistent with the checkpoint's tp_size {tp_size}" - ) - if self.parallel_config.pp_size != 1 and self.parallel_config.pp_size != pp_size: - raise ValueError( - f"pp_size {self.parallel_config.pp_size} is not consistent with the checkpoint's pp_size {pp_size}" - ) - if (self.parallel_config.auto_parallel - and self.parallel_config.world_size != 1 - and world_size != 1): - raise ValueError( - f"auto parallel with world_size {self.parallel_config.world_size} does not support checkpoint with world_size {world_size} > 1" - ) - if not self.parallel_config.auto_parallel: - self.parallel_config = ParallelConfig( - tp_size=tp_size, - pp_size=pp_size, - ) + def handle_generation_msg(self, tensors: tuple, error: str): + super().handle_generation_msg(tensors, error) + if self.tokenizer is not None: + for beam_output in self.outputs: + beam_output.text = self.tokenizer.decode(beam_output.token_ids) -@dataclass(unsafe_hash=True) -class StreamingLLMParam: - # TODO[chunweiy]: optimize the default value - max_attention_window_size: int = 2048 - sink_token_length: int = 4 + def _repr_fields(self): + return [ + 'request_id', 'prompt', 'prompt_token_ids', 'outputs', 'finished' + ] class LLM: - ''' - An end-to-end runner for LLM tasks. - - Classical usage: - - config = ModelConfig() - - llm = LLM(config) - llm.generate(["What is your name?"]) # => ["My name is Llama."] - ''' def __init__(self, - config: ModelConfig, - *, - tokenizer: Optional[TokenizerBase] = None, - dtype: str = 'auto', - kv_cache_config: Optional[KvCacheConfig] = None, - logits_post_processor_map: Optional[Dict[str, Callable[ - [int, torch.Tensor, List[List[int]], int], None]]] = None, - decoding_config: Optional[DecodingConfig] = None, - streaming_llm: Union[bool, StreamingLLMParam] = False, - async_engine_tmp_dir: Optional[str] = None, - **_additional_options: Any): + model: str, + tokenizer: Optional[ + Union[str, Path, TokenizerBase, + 'transformers.PreTrainedTokenizerBase']] = None, + skip_tokenizer_init: bool = False, + tensor_parallel_size: int = 1, + dtype: str = "auto", + revision: Optional[str] = None, + tokenizer_revision: Optional[str] = None, + **kwargs: Any): ''' Args: - config (ModelConfig): - The model config for the model. - tokenizer (TokenizerBase): - User provided tokenizer, will override the default one if exists in the HF model or TRT-LLM engine. - dtype (str): - The data type for the model weights and activations (non-quantized). You can - (1) explicitly specify `float16`, `bfloat16` or `float32`; or - (2) implicitly specify `auto` (default), then `dtype` will be automatically inferred from the source model. However, if the source `dtype` is `float32`, will use `float16` instead. - kv_cache_config (KvCacheConfig): - The config for the paged KV cache. - logits_post_processor_map (Dict[str, Callable[[int, torch.Tensor, List[List[int]], int], None]]): - Optional, a map of logits post processor functions. - decoding_config (DecodingConfig): - Optional, the config for speculative decoding. - streaming_llm (bool, StreamingLLMParam): - Whether to enable the streaming LLM mode. - async_engine_tmp_dir (str): - The temporary directory to save the async engine. Only for debugging. - _additional_params: - Additional options for the model. These options are unstable and are not suggested to be used directly. - - The _additional_params are not suggested to be used directly, ideally the HLAPI will deduce them. They are used for debugging and testing, and may be removed in the future. - The options includes: - normalize_log_probs (bool): - Whether to normalize the log probabilities. - enable_chunked_context (bool): - Whether to enable the chunked context for the generation. - capacity_scheduling_policy (CapacitySchedulerPolicy) - The capacity scheduling policy for the generation. - embedding_parallel_mode (str): - The tensor parallelism mode for embedding module(s). - 'NONE' means parallelim disabled; - 'SHARDING_ALONG_VOCAB' means parallelism enabled with lookup table weight sharded along the vocab dimension; - 'SHARDING_ALONG_HIDDEN' means parallelism enabled with lookup table weight sharded along the hidden dimension. - share_embedding_table (bool): - Whether to share the weight between token embedding lookup table and lm_head. - peft_cache_config (PeftCacheConfig) - The configuration for the peft cache. + model(str): The model name or a local path to the model directory. It could be a HuggingFace(HF) model name, + a local path to the HF model, or a local path to the TRT-LLM engine or checkpoint. + tokenizer(Optional[TokenizerBase]): The tokenizer for the model. + skip_tokenizer_init: If true, skip initialization of tokenizer and detokenizer. generate and generate_async + will accept prompt token ids as input only. + tensor_parallel_size(int): The number of processes for tensor parallelism. + dtype(str): The data type for the model weights and activations. + revision(Optional[str]): The revision of the model. + tokenzier_revision(Optional[str]): The revision of the tokenizer. + + kwargs: Contains the optional arguments for expert users, please refer to `llm_utils.LlmArgs` for more details. ''' + # TODO[chunweiy]: Add API docs - self.config = config + # TODO[chunweiy]: Deal with model_dir - self._tokenizer = tokenizer - self.dtype = dtype - self.async_engine_tmp_dir = async_engine_tmp_dir - self.kv_cache_config = kv_cache_config - self.logits_post_processor_map = logits_post_processor_map - self.decoding_config = decoding_config - # TODO[chunweiy]: add doc for enable_streaming_llm - self.enable_streaming_llm = streaming_llm - if self.enable_streaming_llm is True: - self.enable_streaming_llm = StreamingLLMParam() + try: + self.args = LlmArgs.from_kwargs( + model=model, + tokenizer=tokenizer, + skip_tokenizer_init=skip_tokenizer_init, + tensor_parallel_size=tensor_parallel_size, + dtype=dtype, + revision=revision, + tokenizer_revision=tokenizer_revision, + **kwargs) + except Exception as e: + logger.error( + f"Failed to parse the arguments for the LLM constructor: {e}") + raise e self.mpi_session: Optional[MpiSession] = None - - plugin_config_alterable = self.config.model_format is not _ModelFormatKind.TLLM_ENGINE - - # Read the additional options - self.normalize_log_probs = _additional_options.pop( - 'normalize_log_probs', True) - # Chunked context is enabled by default for performance - self.enable_chunked_context = _additional_options.pop( - 'enable_chunked_context', True if plugin_config_alterable else None) - self.capacity_scheduling_policy = _additional_options.pop( - 'capacity_scheduling_policy', - CapacitySchedulerPolicy.GUARANTEED_NO_EVICT) - self.context_chunking_policy = _additional_options.pop( - 'context_chunking_policy', None) - self.peft_cache_config = _additional_options.pop( - 'peft_cache_config', None) - - self._convert_checkpoint_options = {} - # TODO: Move these options to ParallelConfig - embedding_parallel_mode = _additional_options.pop( - 'embedding_parallel_mode', 'SHARDING_ALONG_VOCAB') - if embedding_parallel_mode == 'NONE': - self._convert_checkpoint_options['use_parallel_embedding'] = False - elif embedding_parallel_mode == 'SHARDING_ALONG_VOCAB': - self._convert_checkpoint_options['use_parallel_embedding'] = True - self._convert_checkpoint_options['embedding_sharding_dim'] = 0 - elif embedding_parallel_mode == 'SHARDING_ALONG_HIDDEN': - self._convert_checkpoint_options['use_parallel_embedding'] = True - self._convert_checkpoint_options['embedding_sharding_dim'] = 1 - else: - raise ValueError( - f"Invalid embedding_parallel_mode: {embedding_parallel_mode}") - self._convert_checkpoint_options[ - 'share_embedding_table'] = _additional_options.pop( - 'share_embedding_table', False) - - if _additional_options: - raise ValueError(f"Unknown options {_additional_options}") - - self.config.parallel_config.devices - if not GpuArch.is_post_ampere(): - logger.info( - f"Disable the chunked context on GPUs that predate the Volta architecture." - ) - self.enable_chunked_context = False - - if self.config.parallel_config.is_multi_gpu: - if get_device_count() < self.config.parallel_config.world_size: + if self.args.parallel_config.is_multi_gpu: + if get_device_count() < self.args.parallel_config.world_size: raise RuntimeError( - f"Only {get_device_count()} GPUs are available, but {self.config.parallel_config.world_size} are required." + f"Only {get_device_count()} GPUs are available, but {self.args.parallel_config.world_size} are required." ) logger.info( - f'start MpiSession with {self.config.parallel_config.world_size} workers' + f'start MpiSession with {self.args.parallel_config.world_size} workers' ) if not external_mpi_comm_available( - self.config.parallel_config.world_size): + self.args.parallel_config.world_size): self.mpi_session = MpiPoolSession( - n_workers=self.config.parallel_config.world_size) + n_workers=self.args.parallel_config.world_size) else: self.mpi_session = MpiCommSession( - n_workers=self.config.parallel_config.world_size) + n_workers=self.args.parallel_config.world_size) - # Due to the gptManager can only accept a engine path, we need to save the engine to a directory - self._engine_dir: Union[tempfile.TemporaryDirectory, str, Path, - None] = None + # Due to the Executor can only accept a engine path, we need to save the engine to a directory + self._engine_dir: Optional[Path] = None self._executor: Optional[GenerationExecutor] = None self._workspace = tempfile.TemporaryDirectory("llm-workspace") self.runtime_context: Optional[_ModelRuntimeContext] = None - - # Update the dependency config if necessary - # When got an engine, the plugin config are fixed, shouldn't be altered. - # TODO[chunweiy]: Refine the rules here and make them easy to be updated through versions - # TODO[chunweiy]: Deal with the rules those depend on each other - - if self.config.model_format is not _ModelFormatKind.TLLM_ENGINE: - if self.enable_streaming_llm: - self.config._update_plugin_config("streamingllm", True) - - self.kv_cache_config = KvCacheConfig( - ) if self.kv_cache_config is None else self.kv_cache_config - self.kv_cache_config.max_attention_window = self.enable_streaming_llm.max_attention_window_size - self.kv_cache_config.sink_token_length = self.enable_streaming_llm.sink_token_length - - # Turn off the conflict perf-optim strategies - if self.kv_cache_config.enable_block_reuse: - logger.warning( - f"Disable KvCacheConfig.enable_block_reuse since it is conflict with StreamingLLM feature." - ) - self.kv_cache_config.enable_block_reuse = False - - if self.enable_chunked_context: - logger.warning( - f"Disable Chunked Context since it is conflict with StreamingLLM feature." - ) - self.enable_chunked_context = False - - self.config._update_plugin_config("use_paged_context_fmha", - False) - - if self.kv_cache_config is not None: - if (not GpuArch.is_post_ampere() - ) and self.kv_cache_config.enable_block_reuse: - logger.warning( - f"Disable KvCacheConfig.enable_block_reuse since it is only supported on GPUs that postdate the Ampere architecture." - ) - self.kv_cache_config.enable_block_reuse = False - - if self.kv_cache_config.enable_block_reuse: - if GpuArch.is_post_volta(): - logger.info( - f"Turn on `use_paged_context_fmha` due to enable_block_reuse" - ) - self.config._update_plugin_config( - "use_paged_context_fmha", True) - if self.config.quant_config.quant_algo is QuantAlgo.FP8: - self.enable_chunked_context = False - self.config._update_plugin_config("use_paged_context_fmha", - False) - if self.enable_chunked_context is not None: - if self.enable_chunked_context is True: - assert GpuArch.is_post_ampere() - self.config._update_plugin_config("use_paged_context_fmha", - True) + self.llm_build_stats = LlmBuildStats() self._build_model() exception_handler.register(self) + @property + def workspace(self) -> Path: + return Path(self._workspace.name) + def generate( self, - prompts: Union[Iterable[str], Iterable[List[int]]], + prompts: Union[str, Iterable[str], List[int], Iterable[List[int]]], sampling_params: Optional[Union[SamplingParams, - List[SamplingParams]]] = None, - ) -> Iterable[GenerationOutput]: - ''' Generate the output for the given inputs. + List[SamplingParams]]] = None + ) -> Union[RequestOutput, List[RequestOutput]]: + ''' Generate output for the given prompts in the synchronous mode. + Synchronous generation accepts either single prompt or batched prompts. Args: - prompts: The raw text or token ids to the model. + prompts: The prompt text or token ids; could be either single prompt or batched prompts. sampling_params: The sampling params for the generation, a default one will be used if not provided. ''' - prompts = list(prompts) + if isinstance(prompts, str) or isinstance(prompts[0], str): + unbatched = isinstance(prompts, str) + else: + unbatched = isinstance(prompts[0], int) - sampling_params = self._prepare_sampling_params(sampling_params) - self._generate_check_arguments(prompts, sampling_params) - results = self._executor.generate(prompts, - sampling_params=sampling_params) + if unbatched: + prompts = [prompts] + + futures = [] + for i, prompt in enumerate(prompts): + if isinstance(sampling_params, list): + sp = sampling_params[i] + else: + sp = sampling_params + future = self.generate_async(prompt, + sampling_params=sp, + streaming=False) + futures.append(future) + + for future in futures: + future.result() - return results + if unbatched: + futures = futures[0] - def generate_async(self, - prompt: Union[str, List[int]], - sampling_params: Optional[SamplingParams] = None, - streaming: bool = False) -> GenerationResult: - ''' Generate in asynchronuous mode. + return futures + + def generate_async( + self, + prompt: Union[str, List[int]], + sampling_params: Optional[SamplingParams] = None, + streaming: bool = False, + ) -> RequestOutput: + ''' Generate output for the given prompt in the asynchronous mode. + Asynchronous generation accepts single prompt only. Args: - prompt: The raw text or token ids to the model. + prompt: The prompt text or token ids; must be single prompt. sampling_params: The sampling params for the generation, a default one will be used if not provided. streaming: Whether to use the streaming mode for the generation. ''' + if isinstance(prompt, str): + prompt_token_ids = self._prepare_prompt_token_ids(prompt) + elif isinstance(prompt, list) and isinstance(prompt[0], int): + prompt_token_ids = prompt + prompt = None + else: + raise TypeError( + f"The prompt must be type str or list of int, but got {type(prompt)}" + ) + sampling_params = self._prepare_sampling_params(sampling_params) - self._generate_check_arguments([prompt], sampling_params) + self._check_arguments(prompt_token_ids, sampling_params) - results = self._executor.generate_async( - prompt, - streaming=streaming, + result = self._executor.generate_async( + prompt_token_ids, sampling_params=sampling_params, + streaming=streaming, ) - return results + return RequestOutput(result, prompt, self.tokenizer) + + def _prepare_prompt_token_ids(self, prompt: str) -> List[int]: + if self.tokenizer is None: + raise ValueError("tokenizer is required to tokenize string prompt") + return self.tokenizer.encode(prompt) def _prepare_sampling_params( - self, - sampling_params: Optional[Union[SamplingParams, - List[SamplingParams]]] = None): + self, + sampling_params: Optional[SamplingParams] = None) -> SamplingParams: if sampling_params is None: if self.tokenizer is None: raise ValueError( @@ -495,72 +224,93 @@ def _prepare_sampling_params( ) return SamplingParams(end_id=self.tokenizer.eos_token_id, pad_id=self.tokenizer.pad_token_id) - if isinstance(sampling_params, SamplingParams): - sampling_params = [sampling_params] - for sp in sampling_params: - if sp.end_id is None: + elif isinstance(sampling_params, SamplingParams): + if sampling_params.end_id is None: if self.tokenizer is None: raise ValueError( "tokenizer is required to reset end_id if it is None, or you can explicitly specify the end_id for sampling_params" ) - sp.end_id = self.tokenizer.eos_token_id - sp.pad_id = self.tokenizer.pad_token_id - if len(sampling_params) == 1: - sampling_params = sampling_params[0] - return sampling_params + sampling_params.end_id = self.tokenizer.eos_token_id + if self.tokenizer.pad_token_id is not None: + sampling_params.pad_id = self.tokenizer.pad_token_id + else: + sampling_params.pad_id = self.tokenizer.eos_token_id + return sampling_params + else: + raise TypeError( + f"The sampling_params must be type SamplingParams or None, but got {type(sampling_params)}" + ) - def _generate_check_arguments(self, prompts, - sampling_params: SamplingParams): - if sampling_params is None: - raise ValueError("The sampling_params should be provided.") + def _check_arguments(self, prompt_token_ids: List[int], + sampling_params: SamplingParams) -> None: - build_config = self.config.build_config + build_config = self.args.build_config + prompt_len = len(prompt_token_ids) - for i, prompt in enumerate(prompts): - if isinstance(sampling_params, list): - sp = sampling_params[i] - else: - sp = sampling_params - if isinstance(prompt, list): - prompt_len = len(prompt) - else: - # TODO(enweiz): move tokenizer from GenerationExecutor to LLM and validate on token ids here - prompt_len = len(prompt.split()) + if prompt_len + sampling_params.max_new_tokens > build_config.max_seq_len: + raise ValueError( + f"The sum of prompt length ({prompt_len}) and max_new_tokens ({sampling_params.max_new_tokens}) should not exceed " + f"max_seq_len ({build_config.max_seq_len})") + if sampling_params.beam_width > build_config.max_beam_width: + raise ValueError( + f"sampling_params's beam_width ({sampling_params.beam_width}) should not exceed max_beam_width ({build_config.max_beam_width})" + ) - if prompt_len + sp.max_new_tokens > build_config.max_seq_len: - raise ValueError( - f"The sum of prompt length ({prompt_len}) and max_new_tokens ({sp.max_new_tokens}) should not exceed " - f"max_seq_len ({build_config.max_seq_len})") - if sp.beam_width > build_config.max_beam_width: - raise ValueError( - f"sampling_params's beam_width ({sp.beam_width}) should not exceed max_beam_width ({build_config.max_beam_width})" - ) + def _build_model(self): + model_loader = CachedModelLoader(self.args, + mpi_session=self.mpi_session, + workspace=self.workspace, + llm_build_stats=self.llm_build_stats) + self._engine_dir = model_loader() + + executor_config = tllm.ExecutorConfig( + max_beam_width=self.args.build_config.max_beam_width, + scheduler_config=self.args.scheduler_config, + batching_type=tllm.BatchingType.INFLIGHT) + if self.args.kv_cache_config is not None: + executor_config.kv_cache_config = self.args.kv_cache_config + if self.args.peft_cache_config is not None: + executor_config.peft_cache_config = self.args.peft_cache_config + if self.args.decoding_config is not None: + executor_config.decoding_config = self.args.decoding_config + if self.args.logits_post_processor_map: + executor_config.logits_post_processor_map = self.args.logits_post_processor_map + executor_config.normalize_log_probs = self.args.normalize_log_probs + executor_config.enable_chunked_context = self.args.enable_chunked_context + executor_config.max_beam_width = self.args.build_config.max_beam_width + + self._executor = GenerationExecutor.create( + self._engine_dir, + executor_config=executor_config, + model_world_size=self.args.parallel_config.world_size, + mpi_session=self.mpi_session, + reuse_mpi_comm=external_mpi_comm_available( + self.args.parallel_config.world_size)) @property def tokenizer(self) -> TokenizerBase: - if self._tokenizer is not None: - return self._tokenizer + if self.args.skip_tokenizer_init: + return None + if self.args.tokenizer is not None: + return self.args.tokenizer if self.runtime_context is not None: return self.runtime_context.tokenizer try: - self._tokenizer = ModelLoader.load_hf_tokenizer( - self.config.model_dir) + self.args.tokenizer = ModelLoader.load_hf_tokenizer( + self.args.model_dir) except: pass - return self._tokenizer + return self.args.tokenizer def save(self, engine_dir: str): ''' Save the built engine to the given path. ''' logger.info(f"Save model to {engine_dir}") if self._engine_dir is None: raise RuntimeError("The engine is not built yet.") - src_engine_dir = self._engine_dir.name if isinstance( - self._engine_dir, tempfile.TemporaryDirectory) else self._engine_dir - - if os.path.abspath(src_engine_dir) != os.path.abspath(engine_dir): - shutil.copytree(src_engine_dir, engine_dir, dirs_exist_ok=True) + if self._engine_dir.absolute() != os.path.abspath(engine_dir): + shutil.copytree(self._engine_dir, engine_dir, dirs_exist_ok=True) def shutdown(self): if hasattr(self, "_executor") and self._executor is not None: @@ -578,550 +328,8 @@ def __exit__(self, exc_type, exc_value, traceback) -> bool: self.shutdown() return exc_type is not None - def _save_engine(self, engine_dir: str): - logger.info(f"Save model to {engine_dir}") - - if self.config.parallel_config.is_multi_gpu: - if self._executor is not None: - self._executor.shutdown() - self.mpi_session.submit_sync(LLM._node_save_task, engine_dir, - self.config.model_dir) - else: - ModelLoader.save(self.runtime_context, - self.config.model_dir, - engine_dir=engine_dir, - model_info=self.runtime_context.model_info) - - def _build_model(self): - model_format = self.config.model_format - self._engine_dir = self.config.model_dir - - def get_engine_dir(): - return self._engine_dir.name if isinstance( - self._engine_dir, - tempfile.TemporaryDirectory) else self._engine_dir - - if model_format is not _ModelFormatKind.TLLM_ENGINE: - - if self._executor is not None: - self._executor.shutdown() - - self._engine_dir = self.async_engine_tmp_dir - if self._engine_dir is None: - self._engine_dir = tempfile.TemporaryDirectory() - - if self.config.parallel_config.is_multi_gpu: - self.mpi_session.submit_sync( - LLM._node_build_task, - self.config, - self._tokenizer, - self.dtype, - self._workspace.name, - build_config=self.config.build_config, - convert_checkpoint_options=self._convert_checkpoint_options, - ) - self._save_engine(get_engine_dir()) - - self.mpi_session.submit_sync(LLM._node_free_state_task) - - else: - - with ModelLoader( - self.config, - tokenizer=self._tokenizer, - dtype=self.dtype, - workspace=self._workspace.name, - build_config=self.config.build_config, - convert_checkpoint_options=self. - _convert_checkpoint_options, - ) as model_loader: - - runtime_context = model_loader() - - # TODO[chunweiy]: Make GptManager support in-memory engine-buffer to save disk loading latency - ModelLoader.save(runtime_context, - self.config.model_dir, - engine_dir=get_engine_dir(), - model_info=runtime_context.model_info) - - # Once saved, the engine_buffer is not needed anymore - del runtime_context - - release_gc() - - tokenizer = self.tokenizer - if not isinstance(tokenizer, TokenizerBase): - tokenizer = ModelLoader.load_hf_tokenizer(self.config.model_dir) - - executor_config = tllm.ExecutorConfig( - max_beam_width=self.config.build_config.max_beam_width, - scheduler_config=tllm.SchedulerConfig( - self.capacity_scheduling_policy, self.context_chunking_policy), - batching_type=tllm.BatchingType.INFLIGHT) - if self.kv_cache_config is not None: - executor_config.kv_cache_config = self.kv_cache_config - if self.peft_cache_config is not None: - executor_config.peft_cache_config = self.peft_cache_config - if self.decoding_config is not None: - executor_config.decoding_config = self.decoding_config - if self.logits_post_processor_map is not None: - executor_config.logits_post_processor_map = self.logits_post_processor_map - executor_config.normalize_log_probs = self.normalize_log_probs - executor_config.enable_chunked_context = self.enable_chunked_context - executor_config.max_beam_width = self.config.build_config.max_beam_width - - self._executor = GenerationExecutor.create( - get_engine_dir(), - tokenizer, - executor_config=executor_config, - model_world_size=self.config.parallel_config.world_size, - mpi_session=self.mpi_session, - reuse_mpi_comm=external_mpi_comm_available( - self.config.parallel_config.world_size)) - - @print_traceback_on_error - @staticmethod - def _node_build_task( - config: ModelConfig, - tokenizer: Optional[TokenizerBase] = None, - dtype: str = 'auto', - workspace: Optional[str] = None, - build_config: Optional[BuildConfig] = None, - convert_checkpoint_options: Optional[dict] = None) -> bool: - if MPINodeState.is_initialized(): - raise RuntimeError("The MPI node is already initialized.") - - with ModelLoader(config, - tokenizer=tokenizer, - dtype=dtype, - workspace=workspace, - build_config=build_config, - convert_checkpoint_options=convert_checkpoint_options - ) as model_loader: - runtime_context = model_loader() - - # Hold the model builder for later use - MPINodeState.state = runtime_context - return True - - @print_traceback_on_error - @staticmethod - def _node_save_task(engine_dir: str, model_dir: str): - runtime_context: _ModelRuntimeContext = MPINodeState.state - if not isinstance(runtime_context, _ModelRuntimeContext): - raise RuntimeError("Model is not built yet.") - - ModelLoader.save(runtime_context, - model_dir, - engine_dir=engine_dir, - model_info=runtime_context.model_info) - - @print_traceback_on_error - @staticmethod - def _node_free_state_task(): - MPINodeState.state = None - # release the large resource explicitly and immediately, since the following LLM pipeline may need a lot of memory - release_gc() - def __getstate__(self): raise RuntimeError("LLM object can not be pickled.") def __del__(self): self.shutdown() - - -class _ModelFormatKind(Enum): - HF = 0 - TLLM_CKPT = 1 - TLLM_ENGINE = 2 - - -@dataclass -class _ModelInfo: - dtype: Optional[str] = None - architecture: Optional[str] = None - - @property - def model_name(self) -> str: - if self.architecture is None: - raise RuntimeError("The architecture is not set yet.") - - return self.architecture - - @classmethod - def from_pretrained_config(cls, config: PretrainedConfig): - return cls(dtype=config.dtype, architecture=config.architecture) - - @classmethod - def from_builder_config_json(cls, config: dict): - if 'version' in config: - # The Dict format is { 'builder_config':..., 'plugin_config':...} - dtype = config['plugin_config']['gpt_attention_plugin'] - else: - dtype = config['pretrained_config']['dtype'] - - return cls(dtype=dtype, architecture=config['builder_config']['name']) - - @classmethod - def from_module(cls, module: Module): - raise NotImplementedError() - - -@dataclass -class _ModelRuntimeContext: - ''' _ModelRuntimeContext holds the minimum runtime resources for running a model. - It could be a runtime cache in MPI nodes. - ''' - engine_buffer: Optional[trt.IHostMemory] = None - tokenizer: Optional[TokenizerBase] = None - # engine_config is only used for saving the engine to disk - engine_config: Optional[Union[dict, EngineConfig]] = None - mapping: Optional[Mapping] = None - model_info: Optional[_ModelInfo] = None - - @property - def engine(self) -> trt.IHostMemory: - assert self.engine_buffer is not None - return self.engine_buffer - - @property - def model_structure(self) -> str: - # "LlaMACausalForLM" or "OPTForCausalLM" and so on - return self.engine_config.pretrained_config['architecture'] - - -class ModelLoader: - ''' The ModelLoader is used to build an end-to-end model from a model config. - It will construct the runtime resources including engine, tokenizer, model runner etc for a single gpu. - ''' - - def __init__(self, - config: ModelConfig, - tokenizer: Optional[TokenizerBase], - dtype: str = 'auto', - workspace: Optional[str] = None, - build_config: Optional[BuildConfig] = None, - convert_checkpoint_options: Optional[dict] = None): - self.config = config - self.tokenizer = tokenizer - self.dtype = dtype - self.workspace = workspace - - assert build_config - self.build_config = build_config - - self.convert_checkpoint_options = {} if convert_checkpoint_options is None else convert_checkpoint_options - self.rank = mpi_rank() if config.parallel_config.is_multi_gpu else 0 - if config.parallel_config.is_multi_gpu and not config.parallel_config.auto_parallel: - self.mapping = Mapping( - tp_size=config.parallel_config.tp_size, - pp_size=config.parallel_config.pp_size, - rank=self.rank, - world_size=config.parallel_config.world_size, - ) - else: - self.mapping = Mapping() - - self._model_pipeline = [] - - self._model_dir = self.config.model_dir - self._model_info: Optional[_ModelInfo] = None - self._model_name = self.config.model - self.auto_parallel_config = AutoParallelConfig( - world_size=config.parallel_config.world_size if config. - parallel_config.auto_parallel else 1) - default_config = self.config.auto_parallel_config - self.auto_parallel_config.set_defaults( - cluster_key=default_config.cluster_key, - cluster_info=default_config.cluster_info, - same_buffer_io=default_config.same_buffer_io, - sharded_io_allowlist=default_config.sharded_io_allowlist, - ) - - # Prepare the model processing pipeline - if isinstance(self.config.model, Module): - ''' Build engine from user provided model ''' - self._model_pipeline.append( - ("Build TensorRT-LLM engine", - self._build_engine_from_inmemory_model)) - return - - if self.config.model_dir is None: - ''' Download HF model if necessary ''' - if self.config.model is None: - raise ValueError( - "Either model_dir or model should be provided to ModelConfig." - ) - self._model_pipeline.append( - ("Downloading HF model", self._download_hf_model)) - - self._model_format = self.config.model_format - - if self._model_format is _ModelFormatKind.HF: - ''' HF -> TRT checkpoints -> engine ''' - self._model_pipeline.append( - ("Loading HF model to memory", self._load_model_from_hf)) - self._model_pipeline.append( - ("Building TRT-LLM engine", self._build_engine)) - elif self._model_format is _ModelFormatKind.TLLM_CKPT: - ''' TRT checkpoints -> engine ''' - self._model_pipeline.append(("Loading TRT checkpoints to memory", - self._load_model_from_ckpt)) - self._model_pipeline.append( - ("Build TRT-LLM engine", self._build_engine)) - elif self._model_format is _ModelFormatKind.TLLM_ENGINE: - ''' TFRT engine ''' - self._model_pipeline.append( - ("Loading TensorRT-LLM engine", self._load_engine_buffer)) - else: - raise ValueError(f"Unknown model format {self._model_format}") - - if self.tokenizer is None: - ''' Use the default tokenizer if no one is provided. ''' - self._model_pipeline.append( - ("Initialize tokenizer", self._load_hf_tokenizer)) - - def __call__(self) -> _ModelRuntimeContext: - if self.config.parallel_config.is_multi_gpu: - torch.cuda.set_device(self.rank) - - n_steps = len(self._model_pipeline) - to_log = self.rank == 0 - - overall_start_time = time.time() - for off, (info, step) in enumerate(self._model_pipeline): - if to_log: - print_colored("Loading Model: ") - print_colored(f"[{off+1}/{n_steps}]\t", 'bold_green') - print_colored(f"{info}\n") - - start_time = time.time() - step() - latency = time.time() - start_time - if to_log: - print_colored("Time: {:.3f}s\n".format(latency), 'grey') - - overall_latency = time.time() - overall_start_time - if to_log: - print_colored("Loading model done.\n", 'bold_green') - print_colored('Total latency: {:.3f}s\n'.format(overall_latency), - 'grey') - - if self._engine_buffer is None: - raise RuntimeError("The engine is not built yet.") - - if not hasattr(self, '_engine_config'): - raise RuntimeError("config is not loaded.") - - config = self._engine_config - - return _ModelRuntimeContext( - tokenizer=self.tokenizer, - engine_buffer=self._engine_buffer, - engine_config=config, - mapping=self.mapping, - model_info=self._model_info, - ) - - def __enter__(self): - return self - - def __exit__(self, exc_type, exc_value, traceback): - for attr_name in dir(self): - if not callable(getattr( - self, attr_name)) and not attr_name.startswith("__"): - if attr_name not in ('model_format', ): - setattr(self, attr_name, None) - - release_gc() - - @property - def model_format(self) -> _ModelFormatKind: - return self._model_format - - # TODO[tali]: Replace this with a lower-level API - @staticmethod - def save( - model: _ModelRuntimeContext, - model_dir: str, - engine_dir: str, - model_info: _ModelInfo, - ): - ''' Save the built engine on a single GPU to the given path. ''' - mapping = model.mapping - rank = mapping.rank - - def copy_hf_tokenizer_data_to_engine_dir(): - # Copy the HF tokenizer stuff to the engine dir so that we can use the engine dir as a standalone model dir supports end-to-end task. - # This is only for HF model for now, not available for users' customized tokenizers. - import shutil - for name in os.listdir(model_dir): - src = os.path.join(model_dir, name) - dst = os.path.join(engine_dir, name) - if name.startswith('tokenizer'): - if os.path.isdir(src): - shutil.copytree(src, dst, dirs_exist_ok=True) - else: - shutil.copy2(src, dst) - - engine = Engine(config=model.engine_config, engine=model.engine) - engine.save(engine_dir) - if rank == 0 and isinstance(model.tokenizer, TransformersTokenizer): - copy_hf_tokenizer_data_to_engine_dir() - - @staticmethod - def get_model_format(model_dir: str) -> _ModelFormatKind: - ''' Get the format of the model. ''' - # TODO: migrate to detect version field in config.json after TRTLLM-256 finished - if Path.exists( - Path(model_dir) / 'config.json') and file_with_glob_exists( - model_dir, 'rank*.safetensors'): - return _ModelFormatKind.TLLM_CKPT - if (Path.exists(Path(model_dir) / 'config.json') - and (file_with_suffix_exists(model_dir, '.bin') - or file_with_suffix_exists(model_dir, '.safetensors'))): - return _ModelFormatKind.HF - if Path.exists( - Path(model_dir) / 'config.json') and file_with_suffix_exists( - model_dir, '.engine'): - return _ModelFormatKind.TLLM_ENGINE - raise ValueError(f"Unknown model format for {model_dir}") - - def _download_hf_model(self): - ''' Download HF model. ''' - assert self.workspace is not None - assert isinstance(self.config.model, str) - self._model_dir = download_hf_model(self.config.model) - self.config.model_dir = self._model_dir - print_colored(f"Downloaded model to {self._model_dir}\n", 'grey') - - def _load_model_from_hf(self): - ''' Load a TRT-LLM model from a HF model. ''' - from ..models import LLaMAForCausalLM - assert self._model_dir is not None - - import transformers - _pretrained_config = transformers.PretrainedConfig.from_json_file( - os.path.join(self._model_dir, 'config.json')) - - model_arch = _pretrained_config.architectures[0] - - # TODO[chunweiy]: add more models if ready - model2struct = dict( - LlamaForCausalLM=LLaMAForCausalLM, - MixtralForCausalLM=LLaMAForCausalLM, - ) - if model_arch not in model2struct: - raise KeyError( - f"Unsupported model architecture: {model_arch}, " - f"only {', '.join(model2struct.keys())} are supported now.") - - model_cls = model2struct[model_arch] - - if self.config.quant_config.quant_mode.has_any_quant(): - assert self.workspace is not None - checkpoint_dir = f"{self.workspace}/quantized-checkpoint" - if self.rank == 0: - model_cls.quantize( - self._model_dir, - checkpoint_dir, - dtype=self.dtype, - mapping=self.mapping, - quant_config=self.config.quant_config, - ) - if self.config.parallel_config.is_multi_gpu: - mpi_barrier() - self.model = model_cls.from_checkpoint(checkpoint_dir, - rank=self.mapping.rank) - else: - self.model = model_cls.from_hugging_face( - self._model_dir, - dtype=self.dtype, - mapping=self.mapping, - quant_config=self.config.quant_config, - load_model_on_cpu= - True, # TODO:TRTLLM-195 to enhance the weights loading memory usage and chose best location - **self.convert_checkpoint_options, - ) - - self.pretrained_config = self.model.config - self._model_info = _ModelInfo.from_pretrained_config( - self.pretrained_config) - - def _load_model_from_ckpt(self): - ''' Load a TRT-LLM model from checkpoint. ''' - self.pretrained_config = PretrainedConfig.from_json_file( - os.path.join(self._model_dir, 'config.json')) - self.pretrained_config.mapping = self.mapping - - architecture = self.pretrained_config.architecture - assert architecture in MODEL_MAP, \ - f"Unsupported model architecture: {architecture}" - model_cls = MODEL_MAP[architecture] - self.model = model_cls.from_checkpoint(self._model_dir, - config=self.pretrained_config) - self._model_info = _ModelInfo.from_pretrained_config( - self.pretrained_config) - - def _build_engine_from_inmemory_model(self): - assert isinstance(self.config.model, Module) - self._model_info = _ModelInfo.from_module(self.model) - - def _build_engine(self): - - self.build_config.update(auto_parallel_config=self.auto_parallel_config) - if self.auto_parallel_config.enabled: - self.model.config.mapping.rank = self.rank - engine = build(self.model, self.build_config) - - self._engine_buffer = engine.engine - self._engine_config = engine.config - self.mapping = self.model.config.mapping - - # delete the model explicitly to free all the build-time resources - self.model = None - - def _load_engine_buffer(self): - # Load engine buffer from disk - engine = Engine.from_dir(self._model_dir) - self._engine_buffer = engine.engine - self._engine_config = engine.config - - def _load_hf_tokenizer(self): - if self._model_dir: - self.tokenizer = ModelLoader.load_hf_tokenizer(self._model_dir) - if self.tokenizer is None: - logger.warning( - f"failed to load HuggingFace tokenizer from {self._model_dir}\n" - "You can also try to copy the tokenizer* files from HuggingFace model to the engine directory manually." - ) - - @staticmethod - def load_extra_build_configs_from_engine( - model_dir: str) -> Optional[Namespace]: - ''' Load the extra build configs from the engine directory, return None if model isn't an engine. ''' - if ModelLoader.get_model_format( - model_dir) is not _ModelFormatKind.TLLM_ENGINE: - return None - - with open(Path(model_dir) / "config.json", "r") as f: - engine_config = json.load(f) - - # TODO[chunweiy]: Remove the following if-check after the engine config is unified. - if 'build_config' not in engine_config: - return None - build_config = engine_config['build_config'] - build_config.pop("plugin_config") - return Namespace(**build_config) - - @staticmethod - def load_hf_tokenizer(model_dir) -> Optional[TransformersTokenizer]: - try: - return TransformersTokenizer.from_pretrained(model_dir, - legacy=False, - padding_side='left', - truncation_side='left', - trust_remote_code=True, - use_fast=True) - except: - return None diff --git a/tensorrt_llm/hlapi/llm_utils.py b/tensorrt_llm/hlapi/llm_utils.py new file mode 100644 index 000000000..fec343611 --- /dev/null +++ b/tensorrt_llm/hlapi/llm_utils.py @@ -0,0 +1,1376 @@ +__all__ = [ + 'LlmArgs', + 'LlmBuildStats', + 'ModelLoader', + '_ModelRuntimeContext', + '_ModelInfo', + '_ParallelConfig', + '_ModelFormatKind', + 'BatchingType', + 'ExecutorConfig', + 'SchedulerConfig', + 'KvCacheConfig', + 'ContextChunkingPolicy', + 'CapacitySchedulerPolicy', + 'BuildConfig', + 'QuantConfig', + 'CachedModelLoader', + 'ConfigArbitrateError', + '_ConfigArbitrator', +] + +import copy +import json +import os +import shutil +import tempfile +import time +from argparse import Namespace +from dataclasses import asdict, dataclass, field, fields +from enum import Enum +from pathlib import Path +from typing import Any, Callable, Dict, List, Optional, Tuple, Union + +import tensorrt as trt +import torch +from tqdm import tqdm +from transformers import PretrainedConfig as HfPretrainedConfig +from transformers import PreTrainedTokenizerBase + +from tensorrt_llm.models.llama.config import LLaMAConfig + +from .._utils import mpi_barrier, mpi_broadcast, mpi_rank, release_gc +from ..auto_parallel import AutoParallelConfig, infer_cluster_config +from ..bindings.executor import (BatchingType, CapacitySchedulerPolicy, + ContextChunkingPolicy, DecodingConfig, + ExecutorConfig, KvCacheConfig, PeftCacheConfig, + SchedulerConfig) +from ..builder import BuildConfig, Engine, EngineConfig, build +from ..logger import logger +from ..mapping import Mapping +from ..models import MODEL_MAP +from ..models.modeling_utils import PretrainedConfig, QuantAlgo, QuantConfig +from ..module import Module +from .build_cache import (BuildCache, CachedStage, + get_build_cache_config_from_env) +from .mpi_session import MPINodeState, MpiSession +from .tokenizer import TokenizerBase, TransformersTokenizer, tokenizer_factory +# TODO[chunweiy]: move the following symbols back to utils scope, and remove the following import +from .utils import (GpuArch, download_hf_model, download_hf_pretrained_config, + file_with_glob_exists, file_with_suffix_exists, + print_colored, print_traceback_on_error) + + +@dataclass +class _ParallelConfig: + ''' The model distribution configs for LLM. ''' + tp_size: int = 1 + pp_size: int = 1 + auto_parallel: bool = False + + _world_size: int = field(default=1, init=False) + _devices: Optional[List[int]] = field(default=None, init=False) + + @property + def devices(self) -> List[int]: + if self._devices is None: + return list(range(self.world_size)) + return self._devices + + @devices.setter + def devices(self, devices: List[int]): + if len(devices) != self.world_size: + raise ValueError( + f"devices {devices} should have the same length as world_size {self.world_size}" + ) + self._devices = devices + + @property + def world_size(self) -> bool: + if self.auto_parallel: + if self.tp_size > 1 or self.pp_size > 1: + raise RuntimeError( + "manually TP and PP are not supported in auto parallel mode." + ) + return self._world_size + + if self._world_size > 1: + raise RuntimeError( + "world_size > 1 is only supported in auto parallel mode.") + return self.tp_size * self.pp_size + + @world_size.setter + def world_size(self, world_size: int): + if self.auto_parallel: + self._world_size = world_size + elif (not self.auto_parallel + ) and world_size != self.tp_size * self.pp_size: + raise ValueError( + f"world_size {world_size} should be equal to tp_size * pp_size {self.tp_size * self.pp_size} " + "in non-auto_parallel mode.\n" + "For non-auto-parallel mode, the world_size is not needed to set" + ) + + @property + def is_multi_gpu(self) -> bool: + return self.world_size > 1 + + +class _ModelFormatKind(Enum): + HF = 0 + TLLM_CKPT = 1 + TLLM_ENGINE = 2 + + +@dataclass +class _ModelInfo: + dtype: Optional[str] = None + architecture: Optional[str] = None + + @property + def model_name(self) -> str: + if self.architecture is None: + raise RuntimeError("The architecture is not set yet.") + + return self.architecture + + @classmethod + def from_pretrained_config(cls, config: PretrainedConfig): + return cls(dtype=config.dtype, architecture=config.architecture) + + @classmethod + def from_builder_config_json(cls, config: dict): + if 'version' in config: + # The Dict format is { 'builder_config':..., 'plugin_config':...} + dtype = config['plugin_config']['gpt_attention_plugin'] + else: + dtype = config['pretrained_config']['dtype'] + + return cls(dtype=dtype, architecture=config['builder_config']['name']) + + @classmethod + def from_module(cls, module: Module): + raise NotImplementedError() + + +@dataclass +class LlmArgs: + ''' + The arguments for constructing a LLM instance. + + Parameters: + model (str or Path): The model name or a local model directory. + Note that if the value could be both a model name or a local model directory, + the local model directory will be prioritized. + + parallel_config (_ParallelConfig): The parallel configuration for the model. + Default is an empty _ParallelConfig instance. + + tokenizer (str, Path, TokenizerBase, PreTrainedTokenizerBase, optional): + The name or path of a HuggingFace Transformers tokenizer, or the loaded tokenizer. + Default is None. + + skip_tokenizer_init (bool): + If true, skip initialization of tokenizer and detokenizer. LLM.generate and + LLM.generate_async will accept prompt token ids as input only. + + tokenizer_revision (str, optional): The revision of the tokenizer to use. + Default is None. + + dtype (str, default="auto"): The data type for the model weights and activations. + Can be "float16", "bfloat16", "float32", or "auto". If "auto", the data type + will be automatically inferred from the source model. If the source data type + is "float32", it will be converted to "float16". + + revision (str, optional): The revision of the model to use. + Default is None. + + build_config (BuildConfig, default=BuildConfig()): The build configuration for the model. + Default is an empty BuildConfig instance. + + quant_config (QuantConfig, default=QuantConfig()): The quantization configuration for the model. + Default is an empty QuantConfig instance. + + embedding_parallel_mode (str, default="SHARDING_ALONG_VOCAB"): The parallel mode for embeddings. + + share_embedding_table (bool, default=False): Whether to share the embedding table. + + kv_cache_config (KvCacheConfig, optional): The key-value cache configuration for the model. + Default is None. + + peft_cache_config (PeftCacheConfig, optional): The PEFT cache configuration for the model. + Default is None. + + decoding_config (DecodingConfig, optional): The decoding configuration for the model. + Default is None. + + logits_post_processor_map (Dict[str, Callable], optional): A map of logit post-processing functions. + Default is None. + + scheduler_config (SchedulerConfig, default=SchedulerConfig()): The scheduler configuration for the model. + Default is an empty SchedulerConfig instance. + + enable_chunked_context (bool, default=False): Whether to enable chunked context for the model. + + normalize_log_probs (bool, default=False): Whether to normalize log probabilities for the model. + + iter_stats_max_iterations (int, optional): The maximum number of iterations for iteration statistics. + Default is None. + + request_stats_max_iterations (int, optional): The maximum number of iterations for request statistics. + Default is None. + + batching_type (BatchingType, optional): The batching type for the model. + Default is None. + + enable_build_cache (str or bool, optional): Whether to enable build caching for the model. + Default is None. + + enable_tqdm (bool, default=False): Whether to display a progress bar during model building. + ''' + + model: Union[str, Path] + + parallel_config: _ParallelConfig = field(default_factory=_ParallelConfig) + + tokenizer: Optional[Union[str, Path, TokenizerBase, + PreTrainedTokenizerBase]] = None + + skip_tokenizer_init: bool = False + + tokenizer_revision: Optional[str] = None + + dtype: str = "auto" + + revision: Optional[str] = None + + # BuildConfig is introduced to give users a familiar interface to configure the model building. + build_config: Optional[BuildConfig] = None + + quant_config: QuantConfig = field(default_factory=QuantConfig) + + # A handful of options from PretrainedConfig + embedding_parallel_mode: str = 'SHARDING_ALONG_VOCAB' + + share_embedding_table: bool = False + + # Several options from ExecutorConfig, expanded here for less hierarchy + kv_cache_config: Optional[KvCacheConfig] = None + + peft_cache_config: Optional[PeftCacheConfig] = None + + # TODO[enweiz]: this might affect medusa, and could be removed in the future for API consistency + decoding_config: Optional[DecodingConfig] = None + + logits_post_processor_map: Optional[Dict[str, Callable]] = None + + scheduler_config: SchedulerConfig = field(default_factory=SchedulerConfig) + + # chunked context is disabled by default, and it is recommended to keep it enabled. + # The underlying implementation might disable it if it is not supported. + enable_chunked_context: bool = False + + normalize_log_probs: bool = False + + iter_stats_max_iterations: Optional[int] = None + + request_stats_max_iterations: Optional[int] = None + + batching_type: Optional[BatchingType] = None + + # Once set, the model will reuse the build_cache + enable_build_cache: Optional[str | bool] = None + + # Display the model building progress bar + enable_tqdm: bool = False + + def __post_init__(self): + + if self.skip_tokenizer_init: + self.tokenizer = None + else: + self.tokenizer = tokenizer_factory(self.tokenizer) + + self._engine_config: Optional[EngineConfig] = None + + self.auto_parallel_config = AutoParallelConfig( + sharded_io_allowlist=[ + "past_key_value_\\d+", + "present_key_value_\\d*", + ], + same_buffer_io={ + "past_key_value_(\\d+)": "present_key_value_\\1", + }, + **infer_cluster_config(), + ) + + self.kv_cache_config = self.kv_cache_config or KvCacheConfig() + + # This is used to hold th options for convert_checkpoint + self._convert_checkpoint_options = {} + + @classmethod + def from_kwargs(cls, **kwargs) -> "LlmArgs": + LlmArgs._check_executor_config_options_consistency() + + parallel_config = _ParallelConfig( + tp_size=kwargs.pop('tensor_parallel_size', 1), + pp_size=kwargs.pop('pipeline_parallel_size', 1), + auto_parallel=kwargs.pop('auto_parallel', False), + ) + # world_size is only used for auto_parallel mode + world_size = kwargs.pop('world_size', 1) + if parallel_config.auto_parallel: + parallel_config.world_size = world_size + + if devices := kwargs.pop('devices', None): + parallel_config.devices = devices + + ret = cls(parallel_config=parallel_config, **kwargs) + ret.setup() + return ret + + @staticmethod + def _check_executor_config_options_consistency(): + # max_beam_width is not included since vague behavior due to lacking the support for dynamic beam width during + # generation + black_list = set(["max_beam_width"]) + executor_config_attrs = set(attr for attr in dir(ExecutorConfig) + if not attr.startswith('_') + and callable(getattr(ExecutorConfig, attr))) + executor_config_attrs -= black_list + llm_args_attr = set([f.name for f in fields(LlmArgs)]) + # NOTE: When cpp ExecutorConfig add new options, please add the new options into `_LlmArgs` with docs as well + # ASK chunweiy for help if you are not sure about the new options. + assert executor_config_attrs.issubset( + llm_args_attr + ), f"New options found in underlying ExecutorConfig: {llm_args_attr - executor_config_attrs}" + + def setup(self): + ''' This method will setup the configs right before building the model. + It will check the consistency of the configs and arbitrate the conflicts. + ''' + + assert isinstance(self.model, + (str, Path)), f"Invalid model: {self.model}" + + self._check_model_or_model_dir() + + self._setup_embedding_parallel_mode() + + if self.is_local_model: + # Load parallel_config from the engine. + self.model_format = ModelLoader.get_model_format(self.model_dir) + if self.model_format is _ModelFormatKind.TLLM_ENGINE: + if self.build_config is not None: + logger.warning( + "The build_config is ignored for model format of TLLM_ENGINE." + ) + self._load_config_from_engine(Path(self.model_dir)) + + # Load parallel_config from the checkpoint. + elif self.model_format is _ModelFormatKind.TLLM_CKPT: + self._load_config_from_ckpt(Path(self.model_dir)) + else: + self.model_format = _ModelFormatKind.HF + + self.build_config = self.build_config or BuildConfig() + + self._config_arbitrator = _ConfigArbitrator() + if self.build_config_mutable: + if not self.build_config.max_num_tokens: + self.build_config.max_num_tokens = 2048 + + if not GpuArch.is_post_ampere(): + self._config_arbitrator.setup("pre-ampere not supported", + config_name="plugin_config", + use_paged_context_fmha=False) + + self._setup_enable_chunked_context() + self._setup_enable_streaming_llm() + self._setup_quant_config() + + if self.build_config.max_beam_width > 1: + self._config_arbitrator.claim_func( + "beam_search (beam_width > 1)", + config_name="kv_cache_config", + enable_block_reuse=False) + + else: + self._setup_build_config_into_config_arbitrator() + + self._setup_kv_cache_config() + + self._config_arbitrator(plugin_config=self.build_config.plugin_config, + kv_cache_config=self.kv_cache_config, + build_config=self.build_config) + + def _check_model_or_model_dir(self): + if not self.model: + raise ValueError("model should be provided.") + assert isinstance(self.model, + (str, Path)), f"Invalid model: {self.model}" + model_dir = Path(self.model) + if model_dir.exists() and model_dir.is_dir(): + self.model = model_dir + + @property + def is_local_model(self) -> bool: + return isinstance(self.model, Path) + + @property + def is_hub_model(self) -> bool: + return not self.is_local_model + + @property + def model_dir(self) -> Path: + assert self.is_local_model + return self.model + + @property + def build_config_mutable(self) -> bool: + return self.model_format is not _ModelFormatKind.TLLM_ENGINE + + def _update_plugin_config(self, key: str, value: Any): + setattr(self.build_config.plugin_config, key, value) + + def _load_config_from_engine(self, engine_dir: Path): + engine_config = EngineConfig.from_json_file(engine_dir / "config.json") + self._pretrained_config = engine_config.pretrained_config + self.build_config = engine_config.build_config + + # load and check parallel_config + mapping = self._pretrained_config.mapping + if self.parallel_config.tp_size not in (1, mapping.tp_size): + raise ValueError( + f"tp_size {self.parallel_config.tp_size} is not consistent with the engine's tp_size {mapping.tp_size}" + ) + if self.parallel_config.pp_size not in (1, mapping.pp_size): + raise ValueError( + f"pp_size {self.parallel_config.pp_size} is not consistent with the engine's pp_size {mapping.pp_size}" + ) + self.parallel_config = _ParallelConfig( + tp_size=mapping.tp_size, + pp_size=mapping.pp_size, + ) + + def _load_config_from_ckpt(self, ckpt_dir: Path): + pretrained_config = PretrainedConfig.from_json_file(ckpt_dir / + "config.json") + tp_size = pretrained_config.mapping.tp_size + pp_size = pretrained_config.mapping.pp_size + world_size = pretrained_config.mapping.world_size + + # load parallel_config + if self.parallel_config.tp_size != 1 and self.parallel_config.tp_size != tp_size: + raise ValueError( + f"tp_size {self.parallel_config.tp_size} is not consistent with the checkpoint's tp_size {tp_size}" + ) + if self.parallel_config.pp_size != 1 and self.parallel_config.pp_size != pp_size: + raise ValueError( + f"pp_size {self.parallel_config.pp_size} is not consistent with the checkpoint's pp_size {pp_size}" + ) + if (self.parallel_config.auto_parallel + and self.parallel_config.world_size != 1 and world_size != 1): + raise ValueError( + f"auto parallel with world_size {self.parallel_config.world_size} does not support checkpoint with " + "world_size {world_size} > 1") + if not self.parallel_config.auto_parallel: + self.parallel_config = _ParallelConfig( + tp_size=tp_size, + pp_size=pp_size, + ) + + def _setup_embedding_parallel_mode(self): + if self.embedding_parallel_mode == 'NONE': + self._convert_checkpoint_options['use_parallel_embedding'] = False + elif self.embedding_parallel_mode == 'SHARDING_ALONG_VOCAB': + self._convert_checkpoint_options['use_parallel_embedding'] = True + self._convert_checkpoint_options['embedding_sharding_dim'] = 0 + elif self.embedding_parallel_mode == 'SHARDING_ALONG_HIDDEN': + self._convert_checkpoint_options['use_parallel_embedding'] = True + self._convert_checkpoint_options['embedding_sharding_dim'] = 1 + else: + raise ValueError( + f"Invalid embedding_parallel_mode: {self.llm_args.embedding_parallel_mode}" + ) + + self._convert_checkpoint_options[ + 'share_embedding_table'] = self.share_embedding_table + + def _setup_build_config_into_config_arbitrator(self): + # Setup the ConfigArbitrator with the plugin_config, the runtime configs such as KvCacheConfig should not be + # conflict with it. + build_config = asdict(self.build_config) + del build_config['plugin_config'] + + self._config_arbitrator.setup("BuildConfig is readonly", + config_name="build_config", + **build_config) + + plugin_config = asdict(self.build_config.plugin_config) + self._config_arbitrator.setup("PluginConfig is readonly", + config_name="plugin_config", + **plugin_config) + + def _setup_enable_chunked_context(self): + + def fallback(): + logger.warning( + f"Disabling chunked context due to configuration conflict.") + self.enable_chunked_context = False + + if self.enable_chunked_context: + if self.build_config_mutable: + self._config_arbitrator.claim_perf("chunked_context", + config_name="plugin_config", + use_paged_context_fmha=True, + fallback=fallback) + + def _setup_enable_streaming_llm(self): + if self.build_config.plugin_config.streamingllm: + self._validate_kv_cache_config() + + self._config_arbitrator.claim_func("streamingllm", + config_name="plugin_config", + streamingllm=True, + use_paged_context_fmha=False) + + self._config_arbitrator.claim_func("streamingllm", + config_name="kv_cache_config", + enable_block_reuse=False) + + def _validate_kv_cache_config(self): + if self.kv_cache_config is None: + raise ValueError("KvCacheConfig is required for streaming LLM.") + + if self.kv_cache_config.max_attention_window is None: + raise ValueError( + "KvCacheConfig.max_attention_window should be set for streaming LLM." + ) + if self.kv_cache_config.max_attention_window <= 0: + raise ValueError( + "KvCacheConfig.max_attention_window should be greater than 0.") + + if self.kv_cache_config.sink_token_length is None: + raise ValueError( + "KvCacheConfig.sink_token_length should be set for streaming LLM." + ) + if self.kv_cache_config.sink_token_length <= 0: + raise ValueError( + "KvCacheConfig.sink_token_length should be greater than 0.") + + def _setup_kv_cache_config(self): + assert self.kv_cache_config is not None + + if not GpuArch.is_post_ampere(): + self._config_arbitrator.setup("pre-ampere not supported", + config_name="kv_cache_config", + enable_block_reuse=False) + + if self.kv_cache_config.enable_block_reuse: + self._config_arbitrator.claim_func("enable_block_reuse", + config_name="kv_cache_config", + enable_block_reuse=True) + self._config_arbitrator.claim_func("enable_block_reuse", + config_name="plugin_config", + use_paged_context_fmha=True) + + def _setup_quant_config(self): + if self.quant_config.quant_algo is QuantAlgo.FP8: + self._config_arbitrator.claim_func("fp8_quant", + config_name="plugin_config", + use_paged_context_fmha=False) + + def __setstate__(self, state): + self.__dict__.update(state) + + def __getstate__(self): + state = self.__dict__.copy() + del state['_config_arbitrator'] + return state + + +class ConfigArbitrateError(Exception): + ''' Exception raised when there is a conflict in configurations. ''' + + def __init__(self, message): + super().__init__(message) + + +class _ConfigArbitrator: + ''' The ConfigArbitrator will arbitrate the options from different sources and raise errors if there are conflicts. ''' + + def __init__(self): + # Dict of configs, the format is {config_name: {option: value}} + self.virtual_configs: Dict[str, Dict[str, Any]] = {} + # The claims for functionalities, the format is {config_name: [(func_name, {option: value})]} + self.func_claims: Dict[str, List[Tuple[str, dict]]] = {} + # The claims for performances, the format is {perf_name: [(config_name, {option: value}, fallback)]}, + # the fallback is a callback function to be called when the performance is abandoned. + self.perf_claims: Dict[str, List[Tuple[str, dict, + Optional[Callable[[], + None]]]]] = {} + # Track where the option settings came from, this will be used for messages when encountered conflicts. + # The format is {config_name: {option: error_information}} + self.option_sources: Dict[str, Dict[str, str]] = {} + + def __call__(self, **configs) -> None: + ''' + Args: + configs: name to config instance for each config need to be arbitrated. + ''' + self._arbitrate() + + # Apply the successfully arbitrated virtual configs to the real configs + for name, config in configs.items(): + if name in self.virtual_configs: + virtual_config = self.virtual_configs[name] + for option, value in virtual_config.items(): + setattr(config, option, value) + + def setup(self, info: str, config_name: str, **kwargs): + ''' Setup with some pre-defined configs comes from environment such as GPU arch. ''' + config = self.virtual_configs.setdefault(config_name, {}) + option_sources = self.option_sources.setdefault(config_name, {}) + for option, value in kwargs.items(): + assert config.get(option, value) == value + config[option] = value + option_sources[option] = info + + def claim_func(self, func: str, config_name: str, **options): + ''' Claim a functionality demanding with configs and options. + The functionality should be fulfilled, or errors will be raised. ''' + + claims = self.func_claims.setdefault(config_name, []) + claims.append((func, options)) + + def claim_perf(self, + perf: str, + config_name: str, + fallback: Optional[Callable[[], None]] = None, + **options): + ''' Claim a performance demanding for configs and options. + The performance could be abandoned if the demanding is not available.''' + claims = self.perf_claims.setdefault(perf, []) + claims.append((config_name, options, fallback)) + + def _arbitrate(self): + ''' Arbitrate the configs for all the functionalities and performances. ''' + + # Resolve functionality claims + for config_name, funcs in self.func_claims.items(): + virtual_config = self.virtual_configs.setdefault(config_name, {}) + option_sources = self.option_sources.setdefault(config_name, {}) + for func, options in funcs: + for option, value in options.items(): + if option in virtual_config: + if virtual_config[option] != value: + existing_func = option_sources[option] + raise ConfigArbitrateError( + f"Cannot set '{option}' to be '{value}' when enabling '{func}', " + f"since '{existing_func}' has set it to be '{virtual_config[option]}'." + ) + else: + virtual_config[option] = value + # Track where the setting came from + option_sources[option] = func + + # copy for restore + # Resolve performance claims + for perf, options in self.perf_claims.items(): + option_sources = copy.copy(self.option_sources) + virtual_configs = copy.copy(self.virtual_configs) + restore = False + for config_name, options, fallback in options: + virtual_config = virtual_configs.setdefault(config_name, {}) + option_source = option_sources.setdefault(config_name, {}) + for option, value in options.items(): + if option in virtual_config and virtual_config[ + option] != value: + logger.warning( + f"Ignoring performance claim '{perf}' for option '{option}' due to conflict." + ) + restore = True + else: + virtual_config[option] = value + option_source[option] = perf + if restore: break + if restore: + if fallback: fallback() + break + + if not restore: + self.option_sources = option_sources + self.virtual_configs = virtual_configs + + +@dataclass +class _ModelRuntimeContext: + ''' _ModelRuntimeContext holds the minimum runtime resources for running a model. + It could be a runtime cache in MPI nodes. + ''' + engine_buffer: Optional[trt.IHostMemory] = None + tokenizer: Optional[TokenizerBase] = None + # engine_config is only used for saving the engine to disk + engine_config: Optional[Union[dict, EngineConfig]] = None + mapping: Optional[Mapping] = None + model_info: Optional[_ModelInfo] = None + + # This is only used when build-cache is enabled + engine_path: Optional[str] = None + + @property + def engine(self) -> trt.IHostMemory: + assert self.engine_buffer is not None + return self.engine_buffer + + @property + def model_arch(self) -> str: + # "LlaMACausalForLM" or "OPTForCausalLM" and so on + return self.engine_config.pretrained_config['architecture'] + + +class ModelLoader: + ''' The ModelLoader is used to build an end-to-end model for a single-gpu. + It accepts model name or a local model dir, and will download the model if necessary. + ''' + + def __init__(self, + llm_args: LlmArgs, + tokenizer: Optional[TokenizerBase], + workspace: Optional[str | tempfile.TemporaryDirectory] = None, + llm_build_stats: Optional["LlmBuildStats"] = None): + self.llm_args = llm_args + self.tokenizer = tokenizer + self._workspace = workspace or tempfile.TemporaryDirectory() + self.llm_build_stats = llm_build_stats or LlmBuildStats() + + assert self.llm_args.build_config + self.build_config = self.llm_args.build_config + + self.convert_checkpoint_options = self.llm_args._convert_checkpoint_options + self.rank = mpi_rank() if llm_args.parallel_config.is_multi_gpu else 0 + if llm_args.parallel_config.is_multi_gpu and not llm_args.parallel_config.auto_parallel: + self.mapping = Mapping( + tp_size=llm_args.parallel_config.tp_size, + pp_size=llm_args.parallel_config.pp_size, + rank=self.rank, + world_size=llm_args.parallel_config.world_size, + ) + else: + self.mapping = Mapping() + + self._build_pipeline = [] + + # For model from hub, the _model_dir is None, and will updated once downloaded + self._model_dir: Optional[ + Path] = self.llm_args.model_dir if self.llm_args.is_local_model else None + self._model_info: Optional[_ModelInfo] = None + self._model_name = self.llm_args.model + self._model_format = self.llm_args.model_format + + self.auto_parallel_config = AutoParallelConfig( + world_size=llm_args.parallel_config.world_size if llm_args. + parallel_config.auto_parallel else 1) + default_config = self.llm_args.auto_parallel_config + self.auto_parallel_config.set_defaults( + cluster_key=default_config.cluster_key, + cluster_info=default_config.cluster_info, + same_buffer_io=default_config.same_buffer_io, + sharded_io_allowlist=default_config.sharded_io_allowlist, + ) + + self._gather_build_steps() + + def _gather_build_steps(self): + # Prepare the model processing pipeline + if isinstance(self.llm_args.model, Module): + # Build engine from user provided model + self._build_pipeline.append( + ("Build TensorRT-LLM engine", + self._build_engine_from_inmemory_model)) + return + + if self.llm_args.is_hub_model and self._model_format is not _ModelFormatKind.TLLM_ENGINE: + # Download HF model if necessary + if self.llm_args.model is None: + raise ValueError( + "Either model_dir or model should be provided to ModelConfig." + ) + self._build_pipeline.append( + ("Downloading HF model", self._download_hf_model)) + + if self._model_format is _ModelFormatKind.HF: + # HF -> TRT checkpoints -> engine + self._build_pipeline.append( + ("Loading HF model to memory", self._load_model_from_hf)) + self._build_pipeline.append( + ("Building TRT-LLM engine", self._build_engine)) + elif self._model_format is _ModelFormatKind.TLLM_CKPT: + # TRT checkpoints -> engine + self._build_pipeline.append(("Loading TRT checkpoints to memory", + self._load_model_from_ckpt)) + self._build_pipeline.append( + ("Build TRT-LLM engine", self._build_engine)) + elif self._model_format is _ModelFormatKind.TLLM_ENGINE: + # Nothing need to do + pass + else: + raise ValueError(f"Unknown model format {self._model_format}") + + class BuildPipeline: + + def __init__(self, enable_tqdm: bool, labels: List[str], + step_handlers: List[Callable], + llm_build_stats: "LlmBuildStats"): + assert len(labels) == len(step_handlers) + self.labels = labels + self.step_handlers = step_handlers + self.llm_build_stats = llm_build_stats + + self.to_log = mpi_rank() == 0 + self.counter = 0 + + self.progress_bar = tqdm( + total=len(labels)) if enable_tqdm and self.to_log else None + + def __call__(self): + start_time = time.time() + + for i in range(len(self.labels)): + self.step_forward() + + if self.to_log: + if self.progress_bar: + self.progress_bar.close() + else: + overall_latency = time.time() - start_time + print_colored("Loading model done.\n", 'bold_green') + print_colored( + 'Total latency: {:.3f}s\n'.format(overall_latency), + 'grey') + + def step_forward(self): + n_steps = len(self.labels) + + label = self.labels[self.counter] + + # display step information + if self.to_log: + if self.progress_bar: + self.progress_bar.set_description(self.labels[self.counter]) + else: + print_colored("Loading Model: ") + print_colored(f"[{self.counter+1}/{n_steps}]\t", + 'bold_green') + print_colored(f"{label}\n") + + # execute the step + start_time = time.time() + self.step_handlers[self.counter]() + + if self.progress_bar: + self.progress_bar.update(1) + + latency = time.time() - start_time + if self.to_log and not self.progress_bar: + print_colored("Time: {:.3f}s\n".format(latency), 'grey') + + self.llm_build_stats.build_steps_info.append((label, latency)) + + self.counter += 1 + + def __call__(self, engine_dir: Optional[Path] = None) -> Path: + ''' + The engine_dir is the path to save the built engine. + ''' + if self.llm_args.model_format is _ModelFormatKind.TLLM_ENGINE: + return self.llm_args.model_dir + + if self.llm_args.parallel_config.is_multi_gpu: + torch.cuda.set_device(self.rank) + + len(self._build_pipeline) + to_log = self.rank == 0 + + pipeline = ModelLoader.BuildPipeline( + self.llm_args.enable_tqdm, + [label for label, _ in self._build_pipeline], + [handler for _, handler in self._build_pipeline], + llm_build_stats=self.llm_build_stats, + ) + pipeline() + + if not hasattr(self, '_engine_config'): + raise RuntimeError("config is not loaded.") + + config = self._engine_config + + assert engine_dir + + runtime_context = _ModelRuntimeContext( + tokenizer=self.tokenizer, + engine_buffer=self._engine_buffer, + engine_config=config, + mapping=self.mapping, + model_info=self._model_info, + ) + ModelLoader.save(runtime_context, self.llm_args.model_dir, engine_dir) + return engine_dir + + def __enter__(self): + return self + + def __exit__(self, exc_type, exc_value, traceback): + for attr_name in dir(self): + if not callable(getattr( + self, attr_name)) and not attr_name.startswith("__"): + if attr_name not in ('model_format', 'workspace'): + setattr(self, attr_name, None) + + release_gc() + + @property + def workspace(self) -> str: + return self._workspace + + @property + def model_format(self) -> _ModelFormatKind: + return self._model_format + + # TODO[tali]: Replace this with a lower-level API + @staticmethod + def save( + model: _ModelRuntimeContext, + model_dir: str, + engine_dir: str, + ): + ''' Save the built engine on a single GPU to the given path. ''' + mapping = model.mapping + rank = mapping.rank + + def copy_hf_tokenizer_data_to_engine_dir(): + # Copy the HF tokenizer stuff to the engine dir so that we can use the engine dir as a standalone model dir + # supports end-to-end task. + # This is only for HF model for now, not available for users' customized tokenizers. + import shutil + for name in os.listdir(model_dir): + src = os.path.join(model_dir, name) + dst = os.path.join(engine_dir, name) + if name.startswith('tokenizer'): + src = os.path.realpath(src) if os.path.islink(src) else src + if os.path.isdir(src): + shutil.copytree(src, dst, dirs_exist_ok=True) + else: + shutil.copy2(src, dst) + + engine = Engine(config=model.engine_config, engine=model.engine) + engine.save(engine_dir) + if rank == 0: + copy_hf_tokenizer_data_to_engine_dir() + + @staticmethod + def get_model_format(model_dir: str) -> _ModelFormatKind: + ''' Get the format of the model. ''' + # TODO: migrate to detect version field in config.json after TRTLLM-256 finished + if Path.exists( + Path(model_dir) / 'config.json') and file_with_glob_exists( + model_dir, 'rank*.safetensors'): + return _ModelFormatKind.TLLM_CKPT + if (Path.exists(Path(model_dir) / 'config.json') + and (file_with_suffix_exists(model_dir, '.bin') + or file_with_suffix_exists(model_dir, '.safetensors'))): + return _ModelFormatKind.HF + if Path.exists( + Path(model_dir) / 'config.json') and file_with_suffix_exists( + model_dir, '.engine'): + return _ModelFormatKind.TLLM_ENGINE + raise ValueError(f"Unknown model format for {model_dir}") + + def _download_hf_model(self): + ''' Download HF model from third-party model hub like www.modelscope.cn or huggingface. ''' + model_dir = None + # Only the rank0 are allowed to download model + if mpi_rank() == 0: + assert self._workspace is not None + assert isinstance(self.llm_args.model, str) + # this will download only once when multiple MPI processes are running + model_dir = download_hf_model(self.llm_args.model, + revision=self.llm_args.revision) + # Make all the processes got the same model_dir + self._model_dir = mpi_broadcast(model_dir, root=0) + self.llm_args.model = Path(self._model_dir) # mark as a local model + print_colored(f"Downloaded model to {self._model_dir}\n", 'grey') + + def _load_model_from_hf(self): + ''' Load a TRT-LLM model from a HF model. ''' + from ..models import LLaMAForCausalLM + assert self._model_dir is not None + + import transformers + _pretrained_config = transformers.PretrainedConfig.from_json_file( + os.path.join(self._model_dir, 'config.json')) + + model_arch = _pretrained_config.architectures[0] + + # TODO[chunweiy]: add more models if ready + model_mapping = dict( + LlamaForCausalLM=LLaMAForCausalLM, + MixtralForCausalLM=LLaMAForCausalLM, + ) + if model_arch not in model_mapping: + raise KeyError( + f"Unsupported model architecture: {model_arch}, " + f"only {', '.join(model_mapping.keys())} are supported now.") + + model_cls = model_mapping[model_arch] + + if self.llm_args.quant_config.quant_mode.has_any_quant(): + assert self.workspace is not None + checkpoint_dir = f"{self.workspace}/quantized-checkpoint" + if self.rank == 0: + model_cls.quantize( + self._model_dir, + checkpoint_dir, + dtype=self.llm_args.dtype, + mapping=self.mapping, + quant_config=self.llm_args.quant_config, + ) + if self.llm_args.parallel_config.is_multi_gpu: + mpi_barrier() + self.model = model_cls.from_checkpoint(checkpoint_dir, + rank=self.mapping.rank) + else: + self.model = model_cls.from_hugging_face( + str(self._model_dir), + dtype=self.llm_args.dtype, + mapping=self.mapping, + quant_config=self.llm_args.quant_config, + load_model_on_cpu= + True, # TODO:TRTLLM-195 to enhance the weights loading memory usage and chose best location + **self.convert_checkpoint_options, + ) + + self.pretrained_config = self.model.config + self._model_info = _ModelInfo.from_pretrained_config( + self.pretrained_config) + + def _load_model_from_ckpt(self): + ''' Load a TRT-LLM model from checkpoint. ''' + self.pretrained_config = PretrainedConfig.from_json_file( + os.path.join(self._model_dir, 'config.json')) + self.pretrained_config.mapping = self.mapping + + architecture = self.pretrained_config.architecture + assert architecture in MODEL_MAP, \ + f"Unsupported model architecture: {architecture}" + model_cls = MODEL_MAP[architecture] + self.model = model_cls.from_checkpoint(self._model_dir, + config=self.pretrained_config) + self._model_info = _ModelInfo.from_pretrained_config( + self.pretrained_config) + + # load embedding sharing related options + self.convert_checkpoint_options[ + 'share_embedding_table'] = self.pretrained_config.share_embedding_table + self.convert_checkpoint_options[ + 'use_parallel_embedding'] = self.pretrained_config.use_parallel_embedding + + def _build_engine_from_inmemory_model(self): + assert isinstance(self.llm_args.model, Module) + self._model_info = _ModelInfo.from_module(self.model) + + def _build_engine(self): + + self.build_config.update(auto_parallel_config=self.auto_parallel_config) + if self.auto_parallel_config.enabled: + self.model.config.mapping.rank = self.rank + assert self.model is not None, "model is loaded yet." + + assert isinstance( + self.build_config, + BuildConfig), f"build_config is not set yet: {self.build_config}" + + engine = build(self.model, self.build_config) + + self._engine_buffer = engine.engine + self._engine_config = engine.config + self.mapping = self.model.config.mapping + + # delete the model explicitly to free all the build-time resources + self.model = None + + def _save_engine_for_runtime(self): + ''' + Persist the engine to disk for the cpp runtime. Currently, the cpp runtime can accept an engine path, + that requires the engine should always be saved to disk. + + This explicit saving will be removed in the future when the cpp runtime can accept the engine buffer directly. + But this is necessary for a build cache, but it can be optimized to async IO. + ''' + if self.build_cache_enabled: + self._model_dir = self.engine_cache_stage.cache_dir + self._model_format = _ModelFormatKind.TLLM_ENGINE + return + + def _load_engine_buffer(self): + # Load engine buffer from disk + engine = Engine.from_dir(self._model_dir) + self._engine_buffer = engine.engine + self._engine_config = engine.config + + @staticmethod + def load_extra_build_configs_from_engine( + model_dir: str) -> Optional[Namespace]: + ''' Load the extra build configs from the engine directory, return None if model isn't an engine. ''' + if ModelLoader.get_model_format( + model_dir) is not _ModelFormatKind.TLLM_ENGINE: + return None + + with open(Path(model_dir) / "config.json", "r") as f: + engine_config = json.load(f) + + # TODO[chunweiy]: Remove the following if-check after the engine config is unified. + if 'build_config' not in engine_config: + return None + build_config = engine_config['build_config'] + build_config.pop("plugin_config") + return Namespace(**build_config) + + @staticmethod + def load_hf_tokenizer(model_dir) -> Optional[TransformersTokenizer]: + try: + return TransformersTokenizer.from_pretrained(model_dir, + legacy=False, + padding_side='left', + truncation_side='left', + trust_remote_code=True, + use_fast=True) + except: + return None + + +class CachedModelLoader: + ''' + The CacheModelLoader is used to build the model in both single or multi-gpu, with cache might be enabled. + ''' + + def __init__( + self, + llm_args: LlmArgs, + llm_build_stats: "LlmBuildStats", + mpi_session: Optional[MpiSession] = None, + workspace: Optional[str] = None, + ): + self.llm_args = llm_args + self.mpi_session = mpi_session + self._workspace = workspace or tempfile.TemporaryDirectory() + self.llm_build_stats = llm_build_stats + + # This is used for build cache. To compute the cache key, a local HF model is required, it could be download + # from HF model hub, so this helps to hold the path. + self._hf_model_dir: Optional[Path] = None + + @property + def workspace(self) -> Path: + return Path(self._workspace.name) if isinstance( + self._workspace, tempfile.TemporaryDirectory) else Path( + self._workspace) + + def __call__(self) -> Path: + + if self.llm_args.model_format is _ModelFormatKind.TLLM_ENGINE: + # do nothing for engine input + return self.llm_args.model_dir + + self.engine_cache_stage: Optional[CachedStage] = None + + if self.build_cache_enabled: + if self.llm_args.is_hub_model: + # This will download the config.json from HF model hub, this helps to create a PretrainedConfig for + # cache key. + self._hf_model_dir = download_hf_pretrained_config( + self.llm_args.model, revision=self.llm_args.revision) + + elif self.llm_args.is_local_model: + self._hf_model_dir = self.llm_args.model_dir if self.llm_args.model_format is _ModelFormatKind.HF else None + + self.engine_cache_stage = self._get_engine_cache_stage() + if self.engine_cache_stage.cache_hitted(): + print_colored( + f"Reusing cached engine in {self.engine_cache_stage.get_engine_path()}\n\n", + 'grey') + self.llm_build_stats.cache_hitted = True + self.llm_args.model = self.engine_cache_stage.get_engine_path() + self.llm_build_stats.engine_dir = self.llm_args.model_dir + return self.llm_build_stats.engine_dir + + return self._build_model() + + def get_engine_dir(self) -> Path: + if self.llm_args.model_format is _ModelFormatKind.TLLM_ENGINE: + return self.llm_args.model_dir + + # generate a new path for writing the engine + if self.build_cache_enabled: + cache_stage = self._get_engine_cache_stage() + return cache_stage.get_engine_path() + + return self.workspace / "tmp.engine" + + @property + def build_cache_enabled(self) -> bool: + _enable_build_cache, _ = get_build_cache_config_from_env() + + return (self.llm_args.enable_build_cache or _enable_build_cache) and ( + self.llm_args.model_format is _ModelFormatKind.HF) + + def _get_engine_cache_stage(self) -> CachedStage: + ''' + Get the cache stage fir engine building. + ''' + _, _build_cache_root = get_build_cache_config_from_env() + build_cache_root = Path(self.llm_args.enable_build_cache if isinstance( + self.llm_args.enable_build_cache, str) else _build_cache_root) + + build_cache = BuildCache(build_cache_root) + + assert self._hf_model_dir is not None, "HF model dir is required for cache key." + dummy_build_config = CachedModelLoader.get_final_build_config( + self.llm_args, self._hf_model_dir) + + return build_cache.get_engine_building_cache_stage( + build_config=dummy_build_config, + model_path=self._hf_model_dir, + # for PretrainedConfig + parallel_config=self.llm_args.parallel_config, + # Other configs affecting the engine building + quant_config=self.llm_args.quant_config) + + @staticmethod + def get_final_build_config(llm_args: LlmArgs, + model_dir: Path) -> BuildConfig: + ''' + Get the build_config for cache key. The tricky part is that, the build_config will be altered in `build()`, + but we need a final version of build_config before `build()` is called for cache key. + + Args: + llm_args: The LlmArgs for building the model. + model_dir: The path to the local HF model. + ''' + + # This is only needed by BuildCache for cache key + # The build() doesn't need the real model instance to get a updated BuildConig. What is really needed is the + # dtype. That's why the model will be downloaded from HF if necessary to get the accurate dtype. + + # TODO[chunweiy]: Support more architectures + hf_pretrained_config = HfPretrainedConfig.from_json_file(model_dir / + "config.json") + if "LlamaForCausalLM" not in hf_pretrained_config.architectures: + raise ValueError( + f"Unsupported model architecture: {hf_pretrained_config.architectures}" + ) + + pretrained_config = LLaMAConfig.from_hugging_face( + model_dir, + mapping=Mapping(world_size=llm_args.parallel_config.world_size, + tp_size=llm_args.parallel_config.tp_size, + pp_size=llm_args.parallel_config.pp_size), + quant_config=llm_args.quant_config, + dtype=llm_args.dtype) + + @dataclass + class DummyModel: + # This is only used for getting the updated BuildConfig from build() without actually loading the whole + # pretrained model to save overhead and memory. + config: LLaMAConfig + + # dry_run to get the updated build_config for cache key. The build_config is modified within build(), so using + # a build_config before build() is not correct for cache key, so we need to get the build_config after build() + # in dry_run mode. + dummy_model = DummyModel(pretrained_config) + dummy_build_config = copy.copy(llm_args.build_config) + dummy_build_config.dry_run = True + updated_build_config = build(dummy_model, + dummy_build_config, + return_build_config=True) + return updated_build_config + + def _build_model(self) -> Path: + model_format = self.llm_args.model_format + + def build_task(): + if model_format is not _ModelFormatKind.TLLM_ENGINE: + + if self.llm_args.parallel_config.is_multi_gpu: + assert self.mpi_session + # The engine_dir:Path will be stored to MPINodeState.state + build_infos = self.mpi_session.submit_sync( + CachedModelLoader._node_build_task, + llm_args=self.llm_args, + tokenizer=self.llm_args. + tokenizer, # TODO[chunweiy]: Use llm_args directly + dtype=self.llm_args.dtype, + engine_dir=self.get_engine_dir()) + self.llm_build_stats.build_steps_info = build_infos[0] + + else: # single-gpu + + with ModelLoader(self.llm_args, + tokenizer=self.llm_args.tokenizer, + workspace=self.workspace.name, + llm_build_stats=self.llm_build_stats + ) as model_loader: + + model_loader(self.get_engine_dir()) + + release_gc() + + if self.build_cache_enabled: + with self.engine_cache_stage.write_guard(): + build_task() + return self.get_engine_dir() + else: + build_task() + + return self.get_engine_dir() + + @print_traceback_on_error + @staticmethod + def _node_build_task( + llm_args: LlmArgs, + tokenizer: Optional[TokenizerBase] = None, + dtype: str = 'auto', + engine_dir: Optional[Path] = None, + ): + if MPINodeState.is_initialized(): + raise RuntimeError("The MPI node is already initialized.") + + with ModelLoader( + llm_args, + tokenizer=tokenizer, + ) as model_loader: + model_loader(engine_dir=engine_dir) + return model_loader.llm_build_stats.build_steps_info + + def save(self, engine_dir: Path): + # copy the engine directory to the target directory + shutil.copytree(self.get_engine_dir(), engine_dir) + + +@dataclass +class LlmBuildStats: + ''' LlmBuildStats is the statistics for the LLM model building. ''' + # Whether the cache is hitted for the engine + cache_hitted: bool = False + + model_from_hf_hub: bool = False + + local_model_dir: Optional[Path] = None + + # The path to the trt-llm engine + engine_dir: Optional[Path] = None + + # The build steps information, including the step name and the latency in seconds. + build_steps_info: List[Tuple[str, float]] = field(default_factory=list) diff --git a/tensorrt_llm/hlapi/tokenizer.py b/tensorrt_llm/hlapi/tokenizer.py index 99cbbc699..88e0056c7 100644 --- a/tensorrt_llm/hlapi/tokenizer.py +++ b/tensorrt_llm/hlapi/tokenizer.py @@ -1,10 +1,7 @@ from pathlib import Path -from typing import Any, List, Union +from typing import Any, List, Optional, Union from transformers import AutoTokenizer, PreTrainedTokenizerBase -from transformers.tokenization_utils_base import PreTrainedTokenizerBase - -TokenIdsTy = List[int] class TokenizerBase(PreTrainedTokenizerBase): @@ -15,13 +12,6 @@ class TransformersTokenizer(TokenizerBase): ''' A wrapper for the Transformers' tokenizer. This is the default tokenizer for LLM. ''' - @classmethod - def from_pretrained(cls, pretrained_model_dir: str, **kwargs): - from transformers import AutoTokenizer - tokenizer = AutoTokenizer.from_pretrained(pretrained_model_dir, - **kwargs) - return TransformersTokenizer(tokenizer) - def __init__(self, tokenizer): self.tokenizer = tokenizer @@ -36,22 +26,31 @@ def eos_token_id(self) -> int: def pad_token_id(self) -> int: return self.tokenizer.pad_token_id - def encode(self, text: str, *args, **kwargs) -> TokenIdsTy: + def encode(self, text: str, *args, **kwargs) -> List[int]: return self.tokenizer.encode(text, *args, **kwargs) - def decode(self, token_ids: TokenIdsTy, *args, **kwargs) -> str: + def decode(self, token_ids: List[int], *args, **kwargs) -> str: return self.tokenizer.decode(token_ids, *args, **kwargs) def batch_encode_plus(self, texts: List[str], *args, **kwargs) -> dict: return self.tokenizer.batch_encode_plus(texts, *args, **kwargs) + def __repr__(self) -> str: + return f"{self.__class__.__name__}({self.tokenizer})" -def tokenizer_factory( - obj: Union[str, Path, TokenizerBase, PreTrainedTokenizerBase, None], - **kwargs) -> Union[TokenizerBase, PreTrainedTokenizerBase, None]: + @classmethod + def from_pretrained(cls, pretrained_model_dir: str, **kwargs): + tokenizer = AutoTokenizer.from_pretrained(pretrained_model_dir, + **kwargs) + return cls(tokenizer) + + +def tokenizer_factory(obj: Optional[Union[str, Path, PreTrainedTokenizerBase, + TokenizerBase]] = None, + **kwargs) -> Optional[TokenizerBase]: if obj is None: return None - if isinstance(obj, (str, Path)): + elif isinstance(obj, (str, Path)): default_kwargs = { 'legacy': False, 'padding_side': 'left', @@ -60,6 +59,10 @@ def tokenizer_factory( 'use_fast': True, } default_kwargs.update(kwargs) - return AutoTokenizer.from_pretrained(obj, **kwargs) - - return obj + return TransformersTokenizer.from_pretrained(obj, **default_kwargs) + elif isinstance(obj, PreTrainedTokenizerBase): + return TransformersTokenizer(obj) + elif isinstance(obj, TokenizerBase): + return obj + else: + raise TypeError(f"Unrecognized tokenizer {obj}") diff --git a/tensorrt_llm/hlapi/utils.py b/tensorrt_llm/hlapi/utils.py index 751aac334..6f8fe9b11 100644 --- a/tensorrt_llm/hlapi/utils.py +++ b/tensorrt_llm/hlapi/utils.py @@ -5,15 +5,16 @@ import tempfile import traceback import weakref -from dataclasses import dataclass, field +from dataclasses import dataclass from functools import wraps from pathlib import Path -from typing import Any, Callable, List, Optional, Union +from typing import Any, Callable, List, Optional import filelock import huggingface_hub import torch from huggingface_hub import snapshot_download +from tqdm.auto import tqdm from tensorrt_llm.bindings import executor as tllme from tensorrt_llm.logger import Singleton, set_level @@ -161,15 +162,6 @@ def _get_output_config(self): for f in expected_fields}) -@dataclass -class GenerationOutput: - text: str = "" - token_ids: Union[List[int], List[List[int]]] = field(default_factory=list) - log_probs: Optional[List[float]] = None - context_logits: Optional[torch.Tensor] = None - generation_logits: Optional[torch.Tensor] = None - - def print_colored(message, color: str = None): colors = dict( grey="\x1b[38;20m", @@ -209,6 +201,10 @@ def get_total_gpu_memory(device: int) -> float: class GpuArch: + @staticmethod + def get_arch() -> int: + return get_gpu_arch() + @staticmethod def is_post_hopper() -> bool: return get_gpu_arch() >= 9 @@ -296,9 +292,29 @@ def get_file_lock(model_name: str, return filelock.FileLock(lock_file_path) -def download_hf_model(model_name: str) -> Path: - with get_file_lock(model_name): +class DisabledTqdm(tqdm): + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs, disable=True) + + +def download_hf_model(model: str, revision: Optional[str] = None) -> Path: + with get_file_lock(model): + hf_folder = snapshot_download( + model, + local_files_only=huggingface_hub.constants.HF_HUB_OFFLINE, + revision=revision, + tqdm_class=DisabledTqdm) + return Path(hf_folder) + + +def download_hf_pretrained_config(model: str, + revision: Optional[str] = None) -> Path: + with get_file_lock(model): hf_folder = snapshot_download( - model_name, - local_files_only=huggingface_hub.constants.HF_HUB_OFFLINE) + model, + local_files_only=huggingface_hub.constants.HF_HUB_OFFLINE, + revision=revision, + allow_patterns=["config.json"], + tqdm_class=DisabledTqdm) return Path(hf_folder) diff --git a/tensorrt_llm/layers/attention.py b/tensorrt_llm/layers/attention.py index f1e505607..34f7c9049 100644 --- a/tensorrt_llm/layers/attention.py +++ b/tensorrt_llm/layers/attention.py @@ -348,7 +348,6 @@ def __init__(self, self.rotary_embedding_scale_type = RotaryScalingType.linear if rotary_embedding_scaling[ "type"] == "linear" else RotaryScalingType.dynamic self.rotary_embedding_scale = rotary_embedding_scaling["factor"] - assert self.rotary_embedding_scale > 1.0 self.rotary_embedding_dim = 0 if self.position_embedding_type.is_rope(): diff --git a/tensorrt_llm/layers/moe.py b/tensorrt_llm/layers/moe.py index 740a680bc..0f3214574 100644 --- a/tensorrt_llm/layers/moe.py +++ b/tensorrt_llm/layers/moe.py @@ -18,14 +18,19 @@ import numpy as np import tensorrt as trt +from packaging import version -from tensorrt_llm._utils import get_init_params, str_dtype_to_trt +from tensorrt_llm._utils import (get_init_params, str_dtype_to_trt, trt_gte_10, + trt_version) from tensorrt_llm.layers.lora import LoraParams from .._common import default_net, default_trtnet +from .._utils import int32_array from ..functional import (AllReduceStrategy, _add_plugin_info, _create_tensor, - allreduce, cast, div, is_gated_activation, - non_gated_version, softmax, sum, topk) + allreduce, cast, concat, constant, div, expand, + gather_nd, is_gated_activation, non_gated_version, + nonzero, repeat_interleave, scatter_nd, shape, + softmax, split, sum, topk) from ..layers import MLP, GatedMLP from ..mapping import Mapping from ..module import Module, ModuleList @@ -80,8 +85,8 @@ def _moe_plugin(moe_config, hidden_states, routing, finished, - expert_weight_1, - expert_weight_2, + expert_weights_1, + expert_weights_2, expert_bias_1, expert_bias_2, expert_scale_1, @@ -113,8 +118,8 @@ def from_parameter(x): return x.value return x - expert_weight_1 = from_parameter(expert_weight_1) - expert_weight_2 = from_parameter(expert_weight_2) + expert_weights_1 = from_parameter(expert_weights_1) + expert_weights_2 = from_parameter(expert_weights_2) expert_bias_1 = from_parameter(expert_bias_1) expert_bias_2 = from_parameter(expert_bias_2) expert_scale_1 = from_parameter(expert_scale_1) @@ -187,7 +192,7 @@ def from_parameter(x): moe_plugin = plugin_creator.create_plugin("mixture_of_experts", pfc) # Instantiate the plugin with our specific inputs - plugin_inputs = [hidden_states, routing, expert_weight_1, expert_weight_2] + plugin_inputs = [hidden_states, routing, expert_weights_1, expert_weights_2] if expert_bias_1: assert expert_bias_2 @@ -417,8 +422,8 @@ def forward_experts(self, hidden_states, routing, finished, output = _moe_plugin(self.moe_config, hidden_states_quant, routing, - expert_weight_1=self.fc.weight.value, - expert_weight_2=self.proj.weight.value, + expert_weights_1=self.fc.weight.value, + expert_weights_2=self.proj.weight.value, expert_bias_1=self.fc.bias, expert_bias_2=self.proj.bias, expert_scale_1=scale_1, @@ -451,12 +456,16 @@ def load_weights(self, moe: "MixtureOfExperts"): def to(self, moe_cls: Type["MixtureOfExperts"], - config=None) -> "MixtureOfExperts": + quant_config=None) -> "MixtureOfExperts": from ..quantization.quantize import quantize + if isinstance(self, moe_cls): + return self new_moe = moe_cls(**get_init_params(self)) - if config is not None: - quantize(new_moe, config.quantization) + # If config is not None, set quantization from config + if quant_config is not None: + quantize(new_moe, quant_config) + new_moe.load_weights(self) new_moe.router = self.router return new_moe @@ -474,16 +483,10 @@ def init_experts(self): ) ClsMLP = GatedMLP if is_gated_activation(self.hidden_act) else MLP - # In OOTB mode, when TP is enabled, using MLP class to do TP settings - # pass self.ffn_hidden_size to original size, - if self.mapping.has_moe_tp(): - tp_size = self.mapping.moe_tp_size - tp_group = self.mapping.moe_tp_group - else: - tp_size = 1 - tp_group = None + tp_size = 1 + tp_group = None self.experts = ModuleList([ - ClsMLP(self.hidden_size, self.ffn_hidden_size, + ClsMLP(self.hidden_size, self.expert_inter_size, non_gated_version(self.hidden_act), self.bias, self.dtype, tp_group, tp_size, self.quant_mode) for _ in range(self.experts_per_node) @@ -532,6 +535,7 @@ def get_params(module): def forward_experts(self, hidden_states, routing, finished, lora_layer_params): + if self.moe_config.normalization_mode == MoeConfig.ExpertScaleNormalizationMode.RENORMALIZE: topk_values, topk_indices = topk(routing, self.top_k, dim=-1) topk_values = softmax(topk_values, -1) @@ -539,29 +543,114 @@ def forward_experts(self, hidden_states, routing, finished, router_probs = softmax(routing, -1) topk_values, topk_indices = topk(router_probs, self.top_k, dim=-1) - output = hidden_states * 0.0 # Create output space - # Experts inference - for i, expert in enumerate(self.experts): - if self.mapping.has_moe_ep(): - index = i + self.experts_per_node * self.mapping.moe_ep_rank - else: - index = i - # inference expert - out = expert(hidden_states, - lora_layer_params=self.moe_to_expert_lora_params( - lora_layer_params, index)) - - expert_mask = topk_indices == index - expert_weights = cast( - sum(topk_values * cast(expert_mask, topk_values.dtype), + if trt_gte_10() and version.parse(trt_version()).minor >= 2: + # For TRT 10.2 and above, avoid over-computing by using NonZero ops to select tokens for each experts. + + hidden_size = shape(hidden_states, -1) + #[B*sq, hidden] + inputs_merged = hidden_states.view(concat([-1, hidden_size])) + flat_topk_indices = topk_indices.view( + concat([-1, shape(topk_indices, -1)])) + flat_topk_values = topk_values.view( + concat([-1, shape(topk_values, -1)])) + + # Create output space + zero_buffer = inputs_merged * 0.0 + output = zero_buffer + + expert_indices_stack = [] + indices_stack = [] + # When topk indices are equal to expert index, the expert will inference the tokens. + # Bundle all indices and experts index, then do mask once. + for i, expert in enumerate(self.experts): + if self.mapping.has_moe_ep(): + index = i + self.experts_per_node * self.mapping.moe_ep_rank + else: + index = i + expert_indices_stack.append( + flat_topk_indices.view(concat([1, + shape(flat_topk_indices)]))) + + indices_stack.append(constant(int32_array(index))) + + all_expert_indices = concat(expert_indices_stack, dim=0) + indices = expand( + concat(indices_stack).view(concat([len(self.experts), 1, 1])), + shape(all_expert_indices)) + + # Create all experts mask + all_expert_mask = all_expert_indices == indices + + experts_weights = cast( + sum(flat_topk_values * + cast(all_expert_mask, flat_topk_values.dtype), dim=-1, keepdim=True), self.dtype) - output += out * expert_weights - if self.mapping.has_moe_ep() and self.mapping.moe_ep_group is not None: - output = allreduce(output, - self.mapping.moe_ep_group, - strategy=AllReduceStrategy.NCCL) + all_expert_mask = cast( + sum(cast(all_expert_mask, flat_topk_values.dtype), + dim=-1, + keepdim=True), 'bool') + all_expert_mask = repeat_interleave(all_expert_mask, + shape(output, -1), 2) + + # split the mask and weights for each expert + experts_mask = split(all_expert_mask, 1, dim=0) + expert_weights = split(experts_weights, 1, dim=0) + + for i, expert in enumerate(self.experts): + # get mask token index + non_zero_index = nonzero(experts_mask[i].view( + concat([-1, hidden_size]))) + non_zero_index = non_zero_index.transpose(1, 0) + input_for_expert = gather_nd(inputs_merged, non_zero_index, 0) + input_for_expert = input_for_expert.view( + concat([-1, hidden_size]), zero_is_placeholder=False) + + # Expert inference + expert_output = expert( + input_for_expert, + lora_layer_params=self.moe_to_expert_lora_params( + lora_layer_params, index)) + + # scatter expert output to real position + expert_finialized_output = zero_buffer + expert_finialized_output = scatter_nd( + expert_finialized_output, non_zero_index, + expert_output.view([-1])) * expert_weights[i] + + output += expert_finialized_output + + output = output.view(shape(hidden_states)) + else: + output = hidden_states * 0.0 # Create output space + # Use over-computation when TRT version is too low. + # Experts inference + for i, expert in enumerate(self.experts): + if self.mapping.has_moe_ep(): + index = i + self.experts_per_node * self.mapping.moe_ep_rank + else: + index = i + # inference expert + out = expert(hidden_states, + lora_layer_params=self.moe_to_expert_lora_params( + lora_layer_params, index)) + + expert_mask = topk_indices == index + expert_weights = cast( + sum(topk_values * cast(expert_mask, topk_values.dtype), + dim=-1, + keepdim=True), self.dtype) + + output += out * expert_weights + + need_ep_reduce = self.mapping.has_moe_ep( + ) and self.mapping.moe_ep_group is not None + need_tp_reduce = self.mapping.has_moe_tp( + ) and self.mapping.moe_tp_group is not None + if need_tp_reduce or need_ep_reduce: + group = self.mapping.moe_ep_group if need_ep_reduce else self.mapping.moe_tp_group + output = allreduce(output, group, strategy=AllReduceStrategy.NCCL) return output @@ -569,8 +658,19 @@ def load_weights(self, moe: MOE): for i, expert in enumerate(self.experts): is_gated_act = is_gated_activation(self.hidden_act) # Gated weight pack in expert1 weights - # expert_weight_1 + # expert_weights_1 experts_weight_1_raw = moe.fc.weight.raw_value + fc1_weight_scale = None + fc1_activation_scale = None + fc2_weight_scale = None + fc2_activation_scale = None + + if self.quant_mode.has_fp8_qdq(): + fc1_weight_scale = moe.fc.weights_scaling_factor.raw_value + fc1_activation_scale = moe.fc.activation_scaling_factor.raw_value + fc2_weight_scale = moe.proj.weights_scaling_factor.raw_value + fc2_activation_scale = moe.proj.activation_scaling_factor.raw_value + if self.quant_mode.is_weight_only(): expert.fc.weight.value = experts_weight_1_raw[ i, :, -self.expert_inter_size:] @@ -584,7 +684,17 @@ def load_weights(self, moe: MOE): expert.gate.weight.value = experts_weight_1_raw[ i, :self.expert_inter_size, :] - # expert_weight_2 + if self.quant_mode.has_fp8_qdq(): + expert.fc.activation_scaling_factor.value = fc1_activation_scale + expert.fc.weights_scaling_factor.value = fc1_weight_scale[i] + expert.proj.activation_scaling_factor.value = fc2_activation_scale + expert.proj.weights_scaling_factor.value = fc2_weight_scale[i] + if is_gated_act: + expert.gate.activation_scaling_factor.value = fc1_activation_scale + expert.gate.weights_scaling_factor.value = fc1_weight_scale[ + i] + + # expert_weights_2 experts_weight_2_raw = moe.proj.weight.raw_value expert.proj.weight.value = experts_weight_2_raw[i, :, :] diff --git a/tensorrt_llm/logger.py b/tensorrt_llm/logger.py index 314525906..7ae3c6dd7 100644 --- a/tensorrt_llm/logger.py +++ b/tensorrt_llm/logger.py @@ -41,6 +41,7 @@ class Logger(metaclass=Singleton): WARNING = '[W]' INFO = '[I]' VERBOSE = '[V]' + DEBUG = '[D]' def __init__(self): environ_severity = os.environ.get('TLLM_LOG_LEVEL') @@ -78,7 +79,7 @@ def _func_wrapper(self, severity): return self._logger.warning elif severity == self.INFO: return self._logger.info - elif severity == self.VERBOSE: + elif severity == self.VERBOSE or severity == self.DEBUG: return self._logger.debug else: raise AttributeError(f'No such severity: {severity}') @@ -132,6 +133,7 @@ def set_level(self, min_severity): 'warning': [trt.Logger.WARNING, logging.WARNING], 'info': [trt.Logger.INFO, logging.INFO], 'verbose': [trt.Logger.VERBOSE, logging.DEBUG], + 'debug': [trt.Logger.VERBOSE, logging.DEBUG], } if G_LOGGER is not None: @@ -141,6 +143,7 @@ def set_level(self, min_severity): 'warning': G_LOGGER.WARNING, 'info': G_LOGGER.INFO, 'verbose': G_LOGGER.SUPER_VERBOSE, + 'debug': G_LOGGER.SUPER_VERBOSE, } for key, value in g_logger_severity_map.items(): severity_map[key].append(value) diff --git a/tensorrt_llm/lora_manager.py b/tensorrt_llm/lora_manager.py index 5896668df..ea93510af 100644 --- a/tensorrt_llm/lora_manager.py +++ b/tensorrt_llm/lora_manager.py @@ -552,6 +552,18 @@ def load_from_hf(self, self.lora_target_modules = lora_target_modules self.missing_qkv_modules = missing_qkv_modules + def preprocess_lora_weights(lora_model): + # Swap weights of gate_up_proj + for key, value in lora_model.items(): + if "gate_up_proj.lora_B.weight" in key: + original_weights = value.contiguous().clone() + half_split = original_weights.shape[0] // 2 + first_half = original_weights[:half_split, :] + second_half = original_weights[half_split:, :] + value = torch.cat((second_half, first_half), dim=0) + lora_model[key] = value + return lora_model + def load_from_model_dir(uid, model_dir, hf_config): if uid not in self._lora_cpp_weights: self._lora_cpp_weights[uid] = [] @@ -560,6 +572,7 @@ def load_from_model_dir(uid, model_dir, hf_config): lora_model = load_state_dict( get_model_path(model_dir, "adapter_model")) + lora_model = preprocess_lora_weights(lora_model) all_weights = get_all_hf_lora_weights(lora_model, hf_modules, component) rank = int(hf_config["r"]) diff --git a/tensorrt_llm/models/convert_utils.py b/tensorrt_llm/models/convert_utils.py index bd0ee680e..5c3c4dfce 100644 --- a/tensorrt_llm/models/convert_utils.py +++ b/tensorrt_llm/models/convert_utils.py @@ -220,6 +220,7 @@ def load_calib_dataset(dataset_name_or_dir: str, config_name: Optional[str] = None, split: Optional[str] = None, key: Optional[str] = None, + trust_remote_code=True, **kwargs): if config_name is None: for name, meta in DEFAULT_HF_DATASET_META.items(): diff --git a/tensorrt_llm/models/gpt/model.py b/tensorrt_llm/models/gpt/model.py index 2b52123e0..f25c47f72 100644 --- a/tensorrt_llm/models/gpt/model.py +++ b/tensorrt_llm/models/gpt/model.py @@ -162,7 +162,8 @@ def forward(self, residual = hidden_states hidden_states = self.post_layernorm(hidden_states) - hidden_states = self.mlp(hidden_states) + hidden_states = self.mlp(hidden_states, + lora_layer_params=lora_layer_params) hidden_states = residual + hidden_states @@ -261,7 +262,15 @@ def __init__(self, config: GPTConfig): gather_output=True) else: lm_head = None + self.trtllm_modules_to_hf_modules = { + "attn_q": "q_proj", + "attn_k": "k_proj", + "attn_v": "v_proj", + "attn_dense": "o_proj", + "mlp_h_to_4h": "c_fc", + "mlp_4h_to_h": "c_proj", + } super().__init__(config, transformer, lm_head) def use_lora(self, lora_config: LoraConfig): - use_lora(self, lora_config) + use_lora(self, lora_config, self.trtllm_modules_to_hf_modules) diff --git a/tensorrt_llm/models/gptneox/model.py b/tensorrt_llm/models/gptneox/model.py index 9007df5b4..0ac5d3563 100644 --- a/tensorrt_llm/models/gptneox/model.py +++ b/tensorrt_llm/models/gptneox/model.py @@ -53,14 +53,16 @@ def __init__(self, config: PretrainedConfig, layer_idx: int): attention_mask_type=AttentionMaskType.causal, bias=True, tp_group=tp_group, - tp_size=tp_size) + tp_size=tp_size, + quant_mode=config.quant_mode) self.mlp = MLP(hidden_size=hidden_size, ffn_hidden_size=hidden_size * 4, hidden_act=config.hidden_act, dtype=dtype, tp_group=tp_group, - tp_size=tp_size) + tp_size=tp_size, + quant_mode=config.quant_mode) def forward(self, hidden_states: Tensor, diff --git a/tensorrt_llm/models/llama/config.py b/tensorrt_llm/models/llama/config.py index 1677727ce..3a40e9232 100644 --- a/tensorrt_llm/models/llama/config.py +++ b/tensorrt_llm/models/llama/config.py @@ -108,6 +108,8 @@ def from_hugging_face( num_key_value_heads = getattr(hf_config, "num_key_value_heads", hf_config.num_attention_heads) + head_dim = hf_config.hidden_size // hf_config.num_attention_heads + head_size = getattr(hf_config, "kv_channels", head_dim) hidden_act = hf_config.hidden_act attn_bias = getattr(hf_config, 'bias', False) or getattr( hf_config, 'attention_bias', False) @@ -153,6 +155,7 @@ def from_hugging_face( hidden_size=hf_config.hidden_size, intermediate_size=hf_config.intermediate_size, num_key_value_heads=num_key_value_heads, + head_size=head_size, vocab_size=hf_config.vocab_size, position_embedding_type='rope_gpt_neox', max_position_embeddings=hf_config.max_position_embeddings, diff --git a/tensorrt_llm/models/llama/convert.py b/tensorrt_llm/models/llama/convert.py index 5f243b926..9b67bcafc 100644 --- a/tensorrt_llm/models/llama/convert.py +++ b/tensorrt_llm/models/llama/convert.py @@ -1266,6 +1266,8 @@ def __init__(self, config: PretrainedConfig): self.tp_size = config.mapping.tp_size self.tp_rank = config.mapping.tp_rank self.is_mha = self.num_heads == self.num_kv_heads + self.head_size = None if not hasattr(config, + "head_size") else config.head_size self._qkv_weights = {} @staticmethod @@ -1301,7 +1303,7 @@ def split_qkv_weights(self, layer_idx): q, k, v = (torch.tensor(weights[t]) for t in ['q', 'k', 'v']) if not self.is_mha: - head_size = self.hidden_size // self.num_heads + head_size = self.hidden_size // self.num_heads if self.head_size is None else self.head_size if self.num_kv_heads < self.tp_size: # duplicate the KV heads up to tensor_parallel k = dup_kv_weight(k, self.num_kv_heads, self.tp_size) diff --git a/tensorrt_llm/models/llama/model.py b/tensorrt_llm/models/llama/model.py index d8fc09b0c..4f1c8aef2 100644 --- a/tensorrt_llm/models/llama/model.py +++ b/tensorrt_llm/models/llama/model.py @@ -27,7 +27,7 @@ from ...quantization import W8A8_SQ_PLUGIN_LIST, QuantAlgo from ..convert_utils import has_safetensors from ..modeling_utils import (DecoderLayerList, DecoderModelForCausalLM, - QuantConfig, preprocess_weights) + QuantConfig, check_share_embedding) from .config import LLaMAConfig from .convert import (load_hf_llama, load_weights_from_hf_by_shard, load_weights_from_hf_model, @@ -323,8 +323,8 @@ def from_hugging_face( else: hf_model = load_hf_llama(hf_model_dir, load_model_on_cpu) weights = load_weights_from_hf_model(hf_model, config) - preprocess_weights(weights, config) + check_share_embedding(weights, config) model = LLaMAForCausalLM(config) model.load(weights) return model @@ -349,8 +349,8 @@ def from_meta_ckpt(cls, **kwargs) weights = load_weights_from_meta_ckpt(meta_ckpt_dir, config) - preprocess_weights(weights, config) + check_share_embedding(weights, config) model = LLaMAForCausalLM(config) model.load(weights) return model diff --git a/tensorrt_llm/models/modeling_utils.py b/tensorrt_llm/models/modeling_utils.py index 15f076d75..e0e2ef9f7 100644 --- a/tensorrt_llm/models/modeling_utils.py +++ b/tensorrt_llm/models/modeling_utils.py @@ -411,8 +411,6 @@ def from_checkpoint(cls, if rank is not None: config.set_rank(rank) - model = cls(config) - weights = None if config.architecture in WEIGHT_LOADER_MODELS: weights_path = os.path.join(ckpt_dir, 'rank0.safetensors') else: @@ -423,12 +421,9 @@ def from_checkpoint(cls, weights = safetensors.torch.load_file(weights_path) is_checkpoint_pruned = getattr(config, 'is_pruned', False) - if weights is not None: - preprocess_weights(weights, - config, - from_pruned=is_checkpoint_pruned) - model.load(weights, from_pruned=is_checkpoint_pruned) - + preprocess_weights(weights, config, from_pruned=is_checkpoint_pruned) + model = cls(config) + model.load(weights, from_pruned=is_checkpoint_pruned) return model def load(self, weights, from_pruned=False): @@ -1002,7 +997,7 @@ def to_ootb_moe(model: PretrainedModel) -> PretrainedModel: for name, layer, parent in model.named_modules_with_parent(): if isinstance(layer, MOE): layer_name = name.rsplit('.', 1)[-1] - ootb_layer = layer.to(MoeOOTB, model.config) + ootb_layer = layer.to(MoeOOTB, model.config.quantization) setattr(parent, layer_name, ootb_layer) return model @@ -1095,7 +1090,14 @@ def optimize_model( def preprocess_weights(weights: Dict[str, torch.Tensor], model_config: PretrainedConfig, - from_pruned=False) -> Dict[str, torch.Tensor]: + from_pruned=False) -> None: + """This function in-place modifies weights and model_config, making them compatible with each other. + + Note: Typically, it should be called before model creation and weight loading. For example, + preprocess_weights(weights, model_config) + model = XXXForCausalLM(model_config) + model.load(weights) + """ quant_algo = model_config.quantization.quant_algo kv_cache_quant_algo = model_config.quantization.kv_cache_quant_algo @@ -1174,6 +1176,11 @@ def preprocess_weights(weights: Dict[str, torch.Tensor], weights[name] = torch.zeros_like(param) # For share_embedding_table + check_share_embedding(weights, model_config) + + +def check_share_embedding(weights: Dict[str, torch.Tensor], + model_config: PretrainedConfig): if model_config.share_embedding_table: if "lm_head.weight" in weights and "transformer.vocab_embedding.weight" in weights: if (weights["lm_head.weight"] - diff --git a/tensorrt_llm/models/opt/model.py b/tensorrt_llm/models/opt/model.py index 615dcfc5f..45fc5d08b 100644 --- a/tensorrt_llm/models/opt/model.py +++ b/tensorrt_llm/models/opt/model.py @@ -48,7 +48,8 @@ def __init__(self, config: PretrainedConfig, layer_idx: int): attention_mask_type=AttentionMaskType.causal, dtype=dtype, tp_group=tp_group, - tp_size=tp_size) + tp_size=tp_size, + quant_mode=config.quant_mode) mlp_hidden_size = hidden_size * 4 if config.intermediate_size is None else config.intermediate_size @@ -57,7 +58,8 @@ def __init__(self, config: PretrainedConfig, layer_idx: int): hidden_act=config.hidden_act, dtype=dtype, tp_group=tp_group, - tp_size=tp_size) + tp_size=tp_size, + quant_mode=config.quant_mode) self.post_layernorm = LayerNorm(normalized_shape=hidden_size, dtype=dtype) diff --git a/tensorrt_llm/models/phi3/model.py b/tensorrt_llm/models/phi3/model.py index 683ad2a5a..f416f6ef4 100644 --- a/tensorrt_llm/models/phi3/model.py +++ b/tensorrt_llm/models/phi3/model.py @@ -12,6 +12,7 @@ from ...functional import PositionEmbeddingType, Tensor from ...layers import (MLP, Attention, AttentionMaskType, BlockSparseAttnParams, Embedding, LayerNorm, ParallelLMHead, RmsNorm) +from ...lora_manager import LoraConfig, use_lora from ...module import Module from ..modeling_utils import (DecoderLayerList, DecoderModelForCausalLM, PretrainedConfig) @@ -69,8 +70,8 @@ def __init__(self, config: PretrainedConfig, layer_idx: int): local_layer_idx = layer_idx - layers_range[0] position_embedding_type = PositionEmbeddingType.rope_gpt_neox - rope_scaling_short_factors, rope_scaling_long_factors = 1.0, 1.0 - rope_scaling_short_mscale, rope_scaling_long_mscale = 1.0, 1.0 + rope_scaling_short_factors, rope_scaling_long_factors = None, None + rope_scaling_short_mscale, rope_scaling_long_mscale = None, None original_max_position_embeddings = config.max_position_embeddings if hasattr(config, "longrope_scaling_short_factors"): @@ -124,6 +125,7 @@ def forward( use_cache=False, kv_cache_params=None, attention_params=None, + lora_layer_params=None, ): input_layernorm_output = self.input_layernorm(hidden_states) @@ -134,6 +136,7 @@ def forward( kv_cache_params=kv_cache_params, attention_params=attention_params, norm_before_bmm1=not self.small_variant, + lora_layer_params=lora_layer_params, ) if use_cache: @@ -141,8 +144,10 @@ def forward( post_attention_input = hidden_states + attention_output post_attention_output = self.post_layernorm(post_attention_input) - feed_forward_hidden_states = self.mlp(post_attention_output, - gegelu_limit=self.gegelu_limit) + feed_forward_hidden_states = self.mlp( + post_attention_output, + gegelu_limit=self.gegelu_limit, + lora_layer_params=lora_layer_params) hidden_states = post_attention_input + feed_forward_hidden_states if use_cache: return (hidden_states, presents) @@ -179,6 +184,7 @@ def forward( prompt_embedding_table=None, prompt_tasks=None, prompt_vocab_size=None, + lora_params=None, ): args = [prompt_embedding_table, prompt_tasks, prompt_vocab_size ] if prompt_embedding_table is not None else [] @@ -193,6 +199,7 @@ def forward( attention_mask=attention_mask, kv_cache_params=kv_cache_params, attention_params=attention_params, + lora_params=lora_params, ) if use_cache: hidden_states, presents = hidden_states @@ -218,7 +225,12 @@ def __init__(self, config: PretrainedConfig): tp_group=config.mapping.tp_group, tp_size=config.mapping.tp_size, gather_output=True) - + self.trtllm_modules_to_hf_modules = { + "attn_qkv": ["qkv_proj", "query_key_value"], + "attn_dense": ["o_proj", "dense"], + "mlp_h_to_4h": ["gate_up_proj", "up_proj"], + "mlp_4h_to_h": "down_proj", + } super().__init__(config, transformer, lm_head) @classmethod @@ -266,3 +278,6 @@ def covert_and_save(rank): assert len( exceptions ) == 0, "Checkpoint conversion failed, please check error log." + + def use_lora(self, lora_config: LoraConfig): + use_lora(self, lora_config, self.trtllm_modules_to_hf_modules) diff --git a/tensorrt_llm/models/qwen/model.py b/tensorrt_llm/models/qwen/model.py index c61f46426..5d4f1e8f5 100644 --- a/tensorrt_llm/models/qwen/model.py +++ b/tensorrt_llm/models/qwen/model.py @@ -233,13 +233,16 @@ def __init__(self, config: PretrainedConfig): lm_head = None self.quant_mode = config.quant_mode self.mapping = config.mapping - self.trtllm_modules_to_hf_modules = { - "attn_qkv": "c_attn", - "attn_dense": "attn.c_proj", - "mlp_h_to_4h": "w2", - "mlp_4h_to_h": "mlp.c_proj", - "mlp_gate": "w1", - } + if config.qwen_type == 'qwen': + self.trtllm_modules_to_hf_modules = { + "attn_qkv": "c_attn", + "attn_dense": "attn.c_proj", + "mlp_h_to_4h": "w2", + "mlp_4h_to_h": "mlp.c_proj", + "mlp_gate": "w1", + } + else: + self.trtllm_modules_to_hf_modules = None super().__init__(config, transformer, lm_head) def check_config(self, config): diff --git a/tensorrt_llm/plugin/plugin.py b/tensorrt_llm/plugin/plugin.py index 4b58f8602..a5263aaa5 100644 --- a/tensorrt_llm/plugin/plugin.py +++ b/tensorrt_llm/plugin/plugin.py @@ -165,7 +165,6 @@ class PluginConfig(metaclass=PluginConfigMeta): _reduce_fusion: bool = field(default=False, init=False) _multi_block_mode: bool = field(default=False, init=False) _enable_xqa: bool = field(default=True, init=False) - _attention_qk_half_accumulation: bool = field(default=False, init=False) _tokens_per_block: int = field(default=64, init=False) _use_paged_context_fmha: bool = field(default=False, init=False) _use_fp8_context_fmha: bool = field(default=False, init=False) @@ -292,7 +291,6 @@ def set_nccl_plugin(self, "use_custom_all_reduce", "multi_block_mode", "enable_xqa", - "attention_qk_half_accumulation", "tokens_per_block", "use_paged_context_fmha", "use_fp8_context_fmha", diff --git a/tensorrt_llm/quantization/functional.py b/tensorrt_llm/quantization/functional.py index 7c73dc2a4..c57ecd0fc 100644 --- a/tensorrt_llm/quantization/functional.py +++ b/tensorrt_llm/quantization/functional.py @@ -236,6 +236,8 @@ def smooth_quant_layer_norm(input: Tensor, bias = constant( np.zeros(normalized_shape, dtype=str_dtype_to_np(p_dtype))) + # LayerNorm plugin only supports float32 scale + scale = cast(scale, "float32") plug_inputs = [ input.trt_tensor, weight.trt_tensor, bias.trt_tensor, scale.trt_tensor @@ -287,6 +289,8 @@ def smooth_quant_rms_norm(input: Tensor, bias = constant( np.zeros(normalized_shape, dtype=str_dtype_to_np(p_dtype))) + # RMS Norm Plugin only supports float32 scale + scale = cast(scale, "float32") plug_inputs = [ input.trt_tensor, weight.trt_tensor, bias.trt_tensor, scale.trt_tensor diff --git a/tensorrt_llm/quantization/layers.py b/tensorrt_llm/quantization/layers.py index b7273b962..0c936bcb2 100644 --- a/tensorrt_llm/quantization/layers.py +++ b/tensorrt_llm/quantization/layers.py @@ -888,9 +888,9 @@ def forward(self, x, lora_runtime_params=None): x.dtype, self.dtype), f"Got input type {x.dtype}, expecting {self.dtype}" - activation_scaling_factor = constant( - self.activation_scaling_factor.raw_value.copy()) - activation_scaling_factor = cast(activation_scaling_factor, self.dtype) + alpha = self.weights_scaling_factor.raw_value * self.activation_scaling_factor.raw_value + activation_scaling_factor = cast(self.activation_scaling_factor.value, + self.dtype) if x.dtype != trt.fp8: quantized_out = quantize(x, activation_scaling_factor, 'fp8') lora_hidden_state = x if lora_runtime_params is not None else None @@ -901,9 +901,8 @@ def forward(self, x, lora_runtime_params=None): x, activation_scaling_factor, -1, self.dtype) if lora_runtime_params is not None else None - weights_scaling_factor = constant( - self.weights_scaling_factor.raw_value.copy()) - weights_scaling_factor = cast(weights_scaling_factor, self.dtype) + weights_scaling_factor = cast(self.weights_scaling_factor.value, + self.dtype) if self.weight.value.dtype != trt.fp8: w_quant_out = quantize(self.weight.value, weights_scaling_factor, 'fp8') @@ -912,7 +911,6 @@ def forward(self, x, lora_runtime_params=None): gemm_plugin = default_net().plugin_config.gemm_plugin if gemm_plugin == 'fp8': - alpha = self.weights_scaling_factor.raw_value * self.activation_scaling_factor.raw_value ret = self.multiply_gather(quantized_out, w_quant_out, gemm_plugin=gemm_plugin, @@ -960,9 +958,9 @@ def forward(self, x, lora_runtime_params=None, reduce_fusion_params=None): assert lora_runtime_params is None or default_net( ).plugin_config.lora_plugin == self.dtype - activation_scaling_factor = constant( - self.activation_scaling_factor.raw_value.copy()) - activation_scaling_factor = cast(activation_scaling_factor, self.dtype) + alpha = self.weights_scaling_factor.raw_value * self.activation_scaling_factor.raw_value + activation_scaling_factor = cast(self.activation_scaling_factor.value, + self.dtype) if x.dtype != trt.fp8: quantized_out = quantize(x, activation_scaling_factor, 'fp8') lora_hidden_state = x if lora_runtime_params is not None else None @@ -973,9 +971,8 @@ def forward(self, x, lora_runtime_params=None, reduce_fusion_params=None): x, activation_scaling_factor, -1, self.dtype) if lora_runtime_params is not None else None - weights_scaling_factor = constant( - self.weights_scaling_factor.raw_value.copy()) - weights_scaling_factor = cast(weights_scaling_factor, self.dtype) + weights_scaling_factor = cast(self.weights_scaling_factor.value, + self.dtype) if self.weight.value.dtype != trt.fp8: w_quant_out = quantize(self.weight.value, weights_scaling_factor, 'fp8') @@ -984,7 +981,6 @@ def forward(self, x, lora_runtime_params=None, reduce_fusion_params=None): gemm_plugin = default_net().plugin_config.gemm_plugin if gemm_plugin == 'fp8': - alpha = self.weights_scaling_factor.raw_value * self.activation_scaling_factor.raw_value ret = self.multiply_reduce( quantized_out, w_quant_out, diff --git a/tensorrt_llm/quantization/quantize.py b/tensorrt_llm/quantization/quantize.py index d4e271749..4b40811d2 100644 --- a/tensorrt_llm/quantization/quantize.py +++ b/tensorrt_llm/quantization/quantize.py @@ -22,7 +22,7 @@ def quantize_layers( preprocess_init_params=None, ): exclude_modules = quant_config.exclude_modules or [ - 'lm_head', + '*lm_head', '*router', '*vocab_embedding', '*position_embedding', diff --git a/tensorrt_llm/quantization/quantize_by_modelopt.py b/tensorrt_llm/quantization/quantize_by_modelopt.py index b71d7590c..ee60e45c7 100644 --- a/tensorrt_llm/quantization/quantize_by_modelopt.py +++ b/tensorrt_llm/quantization/quantize_by_modelopt.py @@ -126,6 +126,7 @@ def quant_cfg_choices(): "ArcticForCausalLM": "llama", "Phi3SmallForCausalLM": "phi3small", "Phi3ForCausalLM": "phi3", + "Starcoder2ForCausalLM": "gptnext", } diff --git a/tensorrt_llm/runtime/generation.py b/tensorrt_llm/runtime/generation.py index 9e6605601..cb03e3f66 100755 --- a/tensorrt_llm/runtime/generation.py +++ b/tensorrt_llm/runtime/generation.py @@ -19,7 +19,7 @@ from dataclasses import dataclass, field from functools import reduce, wraps from pathlib import Path -from typing import Dict, Iterable, List, Optional, Sequence, Union +from typing import Dict, Iterable, List, Optional, Sequence, Set, Union import numpy as np import tensorrt as trt @@ -201,6 +201,8 @@ class _Runtime(object): profiler: _Profiler engine_inspector: trt.EngineInspector cuda_graph_instances: List[cudart.cudaGraphExec_t] + input_tensor_names: Set[str] + output_tensor_names: Set[str] def __init__(self, engine_buffer, mapping: Mapping): self.address = None @@ -243,6 +245,16 @@ def __prepare(self, mapping: Mapping, engine_buffer): self.runtime = trt.Runtime(logger.trt_logger) self.engine = self.runtime.deserialize_cuda_engine(engine_buffer) + + self.input_tensor_names = set() + self.output_tensor_names = set() + for i in range(self.engine.num_io_tensors): + name = self.engine.get_tensor_name(i) + if self.engine.get_tensor_mode(name) == trt.TensorIOMode.OUTPUT: + self.output_tensor_names.add(name) + else: + self.input_tensor_names.add(name) + assert self.engine is not None # The device_memory_size stores the memory required by the largest profile address = CUASSERT(cudart.cudaMalloc(self.engine.device_memory_size))[0] @@ -325,25 +337,30 @@ def _set_buffer(self, context: trt.IExecutionContext, def _set_tensors(self, context: trt.IExecutionContext, tensors: Dict[str, "RuntimeTensor"]): - for i in range(self.engine.num_io_tensors): - name = self.engine.get_tensor_name(i) + for name in self.input_tensor_names: # it's allowed to call set_tensors multi times with different tensors # each time only set some of the engine tensors, so it is valid to skip the ones not in the current given tensors dict if not name in tensors: - if self.engine.get_tensor_mode(name) == trt.TensorIOMode.OUTPUT: - dtype = self.engine.get_tensor_dtype(name) - shape = context.get_tensor_shape(name) - tensors[name] = RuntimeTensor.from_torch( - name, - torch.zeros(tuple(shape), - dtype=trt_dtype_to_torch(dtype), - device='cuda')) - else: - continue + continue + + tensor = tensors[name] + if context.get_tensor_address(name) != tensor.data: + context.set_tensor_address(name, tensor.data) + + if list(context.get_tensor_shape(name)) != tensor.shape: + context.set_input_shape(name, tensor.shape) + + for name in self.output_tensor_names: + if not name in tensors: + dtype = self.engine.get_tensor_dtype(name) + shape = context.get_tensor_shape(name) + tensors[name] = RuntimeTensor.from_torch( + name, + torch.zeros(tuple(shape), + dtype=trt_dtype_to_torch(dtype), + device='cuda')) t = tensors[name] # output's shape is inference by TRT, no need to set the shape here - if self.engine.get_tensor_mode(t.name) == trt.TensorIOMode.INPUT: - context.set_input_shape(t.name, t.shape) context.set_tensor_address(t.name, t.data) def _set_weight_streaming(self, gpu_weights_percent): @@ -697,6 +714,13 @@ def __init__(self, self.vocab_size, self.vocab_size_padded, self.mapping.tp_size, self.mapping.pp_size, self.decoder_logits_dtype) + if self.use_custom_all_reduce and self.mapping.tp_size > 1: + set_peer_access(self.mapping) + self.ipc_buffers, self.all_reduce_workspace = CustomAllReduceHelper.allocate_workspace( + self.mapping, + CustomAllReduceHelper.max_workspace_size_auto( + self.mapping.tp_size)) + self.gather_tree = torch.ops.tensorrt_llm.gather_tree expected_tensor_names = [] @@ -1645,13 +1669,6 @@ def setup(self, self.buffer[f'conv_state_ptr_{i}'] = conv_state_ptr self.buffer[f'rnn_state_ptr_{i}'] = rnn_state_ptr - if self.use_custom_all_reduce and self.mapping.tp_size > 1: - set_peer_access(self.mapping) - self.ipc_buffers, self.all_reduce_workspace = CustomAllReduceHelper.allocate_workspace( - self.mapping, - CustomAllReduceHelper.max_workspace_size_auto( - self.mapping.tp_size)) - if self.use_lora_plugin and self.lora_manager is not None: lora_uids = lora_uids or ["-1"] self.buffer.update( @@ -2203,7 +2220,9 @@ def add_tensor_with_shape(x, name, shape): if self.use_custom_all_reduce and self.mapping.tp_size > 1: add_tensor(self.all_reduce_workspace, 'all_reduce_workspace') - if self.use_lora_plugin: + # Since we are using a ping-pong context design and the lora weight remains constant within the same request, + # it is only necessary to set the lora weight for the first two steps. + if self.use_lora_plugin and step < 2: for idx in range(self.num_layers): layer_idx = idx + self.first_layer for lora_module in (self.lora_target_modules + @@ -3414,7 +3433,8 @@ def decode(self, stop_words_list_ptrs = None max_stop_words_len = 0 if stop_words_list is not None: - stop_words_list = torch.from_numpy(stop_words_list).to('cuda') + stop_words_list = torch.from_numpy(stop_words_list).contiguous().to( + 'cuda') max_stop_words_len = stop_words_list.shape[2] stop_words_lens = torch.full((batch_size, ), max_stop_words_len, @@ -3432,7 +3452,8 @@ def decode(self, bad_words_list_ptrs = None max_bad_words_len = 0 if bad_words_list is not None: - bad_words_list = torch.from_numpy(bad_words_list).to('cuda') + bad_words_list = torch.from_numpy(bad_words_list).contiguous().to( + 'cuda') max_bad_words_len = bad_words_list.shape[2] bad_words_lens = torch.full((batch_size, ), max_bad_words_len, diff --git a/tensorrt_llm/runtime/model_runner_cpp.py b/tensorrt_llm/runtime/model_runner_cpp.py index c3b1b0acb..812329a68 100644 --- a/tensorrt_llm/runtime/model_runner_cpp.py +++ b/tensorrt_llm/runtime/model_runner_cpp.py @@ -63,28 +63,26 @@ def __init__(self, executor: trtllm.Executor, max_batch_size: int, self.world_config = world_config @classmethod - def from_dir( - cls, - engine_dir: str, - *, - lora_dir: Optional[str] = None, - rank: int = 0, - max_batch_size: Optional[int] = None, - max_input_len: Optional[int] = None, - max_output_len: Optional[int] = None, - max_beam_width: Optional[int] = None, - max_attention_window_size: Optional[int] = None, - sink_token_length: Optional[int] = None, - kv_cache_free_gpu_memory_fraction: Optional[float] = None, - medusa_choices: list[list[int]] | None = None, - debug_mode: bool = False, - lora_ckpt_source: str = "hf", - gpu_weights_percent: float = 1, - max_tokens_in_paged_kv_cache: int | None = None, - kv_cache_enable_block_reuse: bool = False, - enable_chunked_context: bool = False, - is_enc_dec: bool = False, - ) -> 'ModelRunnerCpp': + def from_dir(cls, + engine_dir: str, + *, + lora_dir: Optional[str] = None, + rank: int = 0, + max_batch_size: Optional[int] = None, + max_input_len: Optional[int] = None, + max_output_len: Optional[int] = None, + max_beam_width: Optional[int] = None, + max_attention_window_size: Optional[int] = None, + sink_token_length: Optional[int] = None, + kv_cache_free_gpu_memory_fraction: Optional[float] = None, + medusa_choices: list[list[int]] | None = None, + debug_mode: bool = False, + lora_ckpt_source: str = "hf", + gpu_weights_percent: float = 1, + max_tokens_in_paged_kv_cache: int | None = None, + kv_cache_enable_block_reuse: bool = False, + enable_chunked_context: bool = False, + is_enc_dec: bool = False) -> 'ModelRunnerCpp': """ Create a ModelRunnerCpp instance from an engine directory. @@ -343,6 +341,7 @@ def generate(self, output_cum_log_probs: bool = False, prompt_table: Optional[Union[str, torch.Tensor]] = None, prompt_tasks: Optional[str] = None, + return_all_generated_tokens: bool = False, **kwargs) -> Union[torch.Tensor, dict]: """ Generates sequences of token ids. @@ -368,6 +367,8 @@ def generate(self, Custom stopping criteria. logits_processor (LogitsProcessor): Custom logits processors. + return_all_generated_tokens (bool): + Whether the full output is returned at each streaming step kwargs (Dict[str, Any]: Ad hoc parametrization of sampling_config. The passed **kwargs matching the sampling_config's attributes will override them. @@ -442,18 +443,20 @@ def generate(self, len(batch_input_ids_list)) requests = [ - trtllm.Request(input_token_ids=input_ids, - encoder_input_token_ids=encoder_input_ids_list[i] - if encoder_input_ids is not None else None, - max_new_tokens=max_new_tokens, - pad_id=pad_id, - end_id=end_id, - stop_words=stop_words, - bad_words=bad_words, - sampling_config=sampling_config, - streaming=streaming, - output_config=output_config, - prompt_tuning_config=prompt_tuning_config) + trtllm.Request( + input_token_ids=input_ids, + encoder_input_token_ids=encoder_input_ids_list[i] + if encoder_input_ids is not None else None, + max_new_tokens=max_new_tokens, + pad_id=pad_id, + end_id=end_id, + stop_words=stop_words, + bad_words=bad_words, + sampling_config=sampling_config, + streaming=streaming, + output_config=output_config, + prompt_tuning_config=prompt_tuning_config, + return_all_generated_tokens=return_all_generated_tokens) for i, (input_ids, stop_words, bad_words, prompt_tuning_config) in enumerate( zip(batch_input_ids_list, stop_words_list, @@ -463,17 +466,16 @@ def generate(self, request_ids = self.session.enqueue_requests(requests) if not streaming: - return self._initialize_and_fill_output(request_ids, end_id, - return_dict, - output_sequence_lengths, - output_log_probs, - output_cum_log_probs, - batch_input_ids, streaming) + return self._initialize_and_fill_output( + request_ids, end_id, return_dict, output_sequence_lengths, + output_log_probs, output_cum_log_probs, batch_input_ids, + streaming, return_all_generated_tokens) else: return self._stream(request_ids, end_id, return_dict, output_sequence_lengths, output_log_probs, output_cum_log_probs, batch_input_ids, - streaming, batch_input_ids_list) + streaming, batch_input_ids_list, + return_all_generated_tokens) def _prepare_words_list(self, words_list: List[List[List[int]]], batch_size: int): @@ -507,7 +509,7 @@ def _prepare_ptuning_executor(self, batch_input_ids_list, prompt_table, def _initialize_and_fill_output(self, request_ids, end_id, return_dict, output_sequence_lengths, output_log_probs, output_cum_log_probs, batch_input_ids, - streaming): + streaming, return_all_generated_tokens): output_ids = [[] for _ in range(len(request_ids))] for reqid_pos in range(len(request_ids)): output_ids[reqid_pos] = [[] for _ in range(self.max_beam_width)] @@ -520,11 +522,12 @@ def _initialize_and_fill_output(self, request_ids, end_id, return_dict, return self._fill_output(responses, output_ids, end_id, return_dict, output_sequence_lengths, output_log_probs, output_cum_log_probs, batch_input_ids, - streaming, request_ids) + streaming, request_ids, + return_all_generated_tokens) def _stream(self, request_ids, end_id, return_dict, output_sequence_lengths, output_log_probs, output_cum_log_probs, batch_input_ids, - streaming, batch_input_ids_list): + streaming, batch_input_ids_list, return_all_generated_tokens): output_ids = [[] for _ in range(len(request_ids))] for reqid_pos in range(len(request_ids)): output_ids[reqid_pos] = [ @@ -543,12 +546,13 @@ def _stream(self, request_ids, end_id, return_dict, output_sequence_lengths, yield self._fill_output(responses, output_ids, end_id, return_dict, output_sequence_lengths, output_log_probs, output_cum_log_probs, batch_input_ids, - streaming, request_ids) + streaming, request_ids, + return_all_generated_tokens) def _fill_output(self, responses, output_ids, end_id, return_dict, output_sequence_lengths, output_log_probs, output_cum_log_probs, batch_input_ids, streaming, - request_ids): + request_ids, return_all_generated_tokens): cuda_device = torch.device("cuda") for response in responses: @@ -558,7 +562,10 @@ def _fill_output(self, responses, output_ids, end_id, return_dict, reqid_pos = request_ids.index(response.request_id) for beam, output_tokens in enumerate( response.result.output_token_ids): - output_ids[reqid_pos][beam] += output_tokens + if return_all_generated_tokens: + output_ids[reqid_pos][beam] = output_tokens + else: + output_ids[reqid_pos][beam] += output_tokens sequence_lengths = [] for output in output_ids: diff --git a/tensorrt_llm/version.py b/tensorrt_llm/version.py index cb051e10d..ac57fa0b2 100644 --- a/tensorrt_llm/version.py +++ b/tensorrt_llm/version.py @@ -12,4 +12,4 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.11.0.dev2024062500" +__version__ = "0.12.0.dev2024070200" diff --git a/tests/attention/test_gpt_attention.py b/tests/attention/test_gpt_attention.py index d28cda04e..5dbdda00b 100644 --- a/tests/attention/test_gpt_attention.py +++ b/tests/attention/test_gpt_attention.py @@ -286,77 +286,29 @@ def load_test_cases(): test_cases = [(f"partition{int(i % 4)}", ) + case for i, case in enumerate(test_cases)] - # tests (h100 only) - # For warp-specialized kernels on Hopper. + # For HMMA/QGMMA XQA kernels. + # + # Needs to test on both Hopper and non-Hopper, because Hopper may use different kernel. + test_cases += list( product( - ['h100_only_part_0'], - [90], + ['xqa_generic'], + ['all'], ['llama_attention'], [ - ContextFMHAType.enabled, - ContextFMHAType.enabled_with_fp32_acc + ContextFMHAType.disabled, ], ['float16', 'bfloat16'], - [None], - [4], - [68, 2543], - [16], - [32, 64, 96, 112, 128, 160, 192, 256], - [2], - [False], + ['fp8', 'int8', None], + [2], # batch_size + [165], # in_len + [2, 8, 32], # num_q_heads + [32, 64, 96, 128, 160], # head_size + [2], # num_kv_heads + [False], # enable_multi_block_mmha [False], - [1], - [False], - [False], - [10000.0], # rope base - [ # rope scaling - None, - ])) - - # For XQA kernels - # all arches use the same kernel traits, we can assign some workloads to h100-only. - test_cases += list( - product( - ['h100_only_part_1'], - [90], - ['llama_attention'], - [ContextFMHAType.enabled], - ['float16', 'bfloat16'], - [None], - [2], - [165, 1025, 2543], - [16], - [128], - [2], - [False, True], - [False], - [1], - [False, True], - [False], - [10000.0], # rope base - [ # rope scaling - None, - ])) - - # d = 256 xqa kernels (limited number of tests). - test_cases += list( - product( - ['h100_only_part_2'], - [90], - ['llama_attention'], - [ContextFMHAType.enabled], - ['float16', 'bfloat16'], - ['int8', 'fp8'], - [2], - [1025], - [16], - [256], - [2], - [False, True], - [False], - [1], - [False, True], + [1, 2, 4], # beam_width + [False, True], # paged_kv_cache [False], [10000.0], # rope base [ # rope scaling @@ -392,6 +344,7 @@ def test_gpt_attention(self, os.environ['TRTLLM_FORCE_XQA'] = '1' use_int8_kv_cache = True if kv_cache_dtype == 'int8' else False use_fp8_kv_cache = True if kv_cache_dtype == 'fp8' else False + output_atol = 2e-2 if kv_cache_dtype == 'int8' else 2e-3 if kv_cache_dtype is None: kv_cache_dtype = dtype # skip tests based on the gpu_arch_lists @@ -1354,7 +1307,7 @@ def tile_beam_width(tensor: torch.Tensor, num_beams: int): np.testing.assert_allclose( torch.flatten(tiled_output).to(torch.float32).cpu().numpy(), torch.flatten(torch_output).to(torch.float32).cpu().numpy(), - atol=2e-3) + atol=output_atol) if paged_kv_cache: # Iterate to the next step. Increase number of tokens for all unfinished sequences diff --git a/tests/bindings/test_executor_bindings.py b/tests/bindings/test_executor_bindings.py index 0e49f1f18..a9b06201c 100644 --- a/tests/bindings/test_executor_bindings.py +++ b/tests/bindings/test_executor_bindings.py @@ -912,6 +912,8 @@ def test_speculative_decoding_config(): def test_executor_config(): config = trtllm.ExecutorConfig() assert config.max_beam_width == 1 + assert config.max_batch_size is None + assert config.max_num_tokens is None assert isinstance(config.scheduler_config, trtllm.SchedulerConfig) assert isinstance(config.kv_cache_config, trtllm.KvCacheConfig) assert config.enable_chunked_context == False @@ -927,6 +929,10 @@ def test_executor_config(): kwargs = { "max_beam_width": 2, + "max_batch_size": + 8, + "max_num_tokens": + 128, "scheduler_config": trtllm.SchedulerConfig(trtllm.CapacitySchedulerPolicy.MAX_UTILIZATION), "kv_cache_config": @@ -1131,6 +1137,7 @@ def test_iteration_stats(): stats.iter = 1 stats.iter_latency_ms = 100 stats.num_active_requests = 2 + stats.num_queued_requests = 10 stats.max_num_active_requests = 3 stats.gpu_mem_usage = 1024 stats.cpu_mem_usage = 2048 @@ -1140,6 +1147,7 @@ def test_iteration_stats(): assert stats_json["iter"] == stats.iter assert stats_json["iterLatencyMS"] == stats.iter_latency_ms assert stats_json["numActiveRequests"] == stats.num_active_requests + assert stats_json["numQueuedRequests"] == stats.num_queued_requests assert stats_json["maxNumActiveRequests"] == stats.max_num_active_requests assert stats_json["gpuMemUsage"] == stats.gpu_mem_usage assert stats_json["cpuMemUsage"] == stats.cpu_mem_usage @@ -1231,3 +1239,14 @@ def test_executor_config_pickle(): assert config.max_beam_width == config_copy.max_beam_width assert config.scheduler_config.capacity_scheduler_policy == config_copy.scheduler_config.capacity_scheduler_policy assert config.kv_cache_config.enable_block_reuse == config_copy.kv_cache_config.enable_block_reuse + + +def test_return_full_tokens(): + max_new_tokens = 5 + input_tokens = [1, 2, 3, 4] + request = trtllm.Request(input_tokens, max_new_tokens, False, + trtllm.SamplingConfig()) + request.return_all_generated_tokens = True + assert request.return_all_generated_tokens == True + request.return_all_generated_tokens = False + assert request.return_all_generated_tokens == False diff --git a/tests/functional/test_moe.py b/tests/functional/test_moe.py index 1c7609c49..f2c9920bb 100644 --- a/tests/functional/test_moe.py +++ b/tests/functional/test_moe.py @@ -32,8 +32,9 @@ from tensorrt_llm import Tensor from tensorrt_llm._utils import (torch_to_numpy, trt_dtype_to_str, trt_dtype_to_torch) -from tensorrt_llm.layers.moe import MoeConfig -from tensorrt_llm.quantization import QuantMode +from tensorrt_llm.layers.moe import MoeConfig, MoeOOTB +from tensorrt_llm.models.modeling_utils import QuantConfig +from tensorrt_llm.quantization import QuantAlgo, QuantMode sys.path.append(os.path.join(os.path.dirname(__file__), '..')) from utils.util import (create_session, getSMVersion, run_session, @@ -630,8 +631,7 @@ def create_trt_session(self, max_sizes=None): builder = tensorrt_llm.Builder() network = builder.create_network() - if use_plugin: - network.plugin_config.moe_plugin = trt_dtype_to_str(dtype) + with tensorrt_llm.net_guard(network): if max_sizes: dim_range = OrderedDict([("max_num_seq", [[1, 1, @@ -647,10 +647,13 @@ def create_trt_session(self, dim_range=dim_range, dtype=dtype) - moe = tensorrt_llm.layers.MOE(moe_config=MoeConfig( - num_experts=num_experts, - top_k=top_k, - normalization_mode=norm_mode), + network.plugin_config.moe_plugin = trt_dtype_to_str(dtype) + + moe_config = MoeConfig(num_experts=num_experts, + top_k=top_k, + normalization_mode=norm_mode) + + moe = tensorrt_llm.layers.MOE(moe_config=moe_config, hidden_size=hidden_size, ffn_hidden_size=ffn_hidden_size, hidden_act=actfn, @@ -681,6 +684,14 @@ def create_trt_session(self, if custom_network: custom_network(network, trt_key) + if not use_plugin: + quant_config = None + if quant_mode.has_fp8_qdq(): + quant_config = QuantConfig( + quant_algo=QuantAlgo.FP8, + kv_cache_quant_algo=QuantAlgo.FP8) + moe = moe.to(MoeOOTB, quant_config=quant_config) + output = moe(trt_key) output.mark_output('output', dtype) diff --git a/tests/functional/test_scatter_nd.py b/tests/functional/test_scatter_nd.py new file mode 100644 index 000000000..81857bd96 --- /dev/null +++ b/tests/functional/test_scatter_nd.py @@ -0,0 +1,85 @@ +# SPDX-FileCopyrightText: Copyright (c) 2022-2024 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +import os +import sys +import unittest +from itertools import product + +import torch +from parameterized import parameterized + +import tensorrt_llm +from tensorrt_llm import Tensor +from tensorrt_llm._utils import str_dtype_to_torch + +sys.path.append(os.path.join(os.path.dirname(__file__), '..')) +from utils.util import create_session, run_session + + +class TestScatter(unittest.TestCase): + + def setUp(self): + tensorrt_llm.logger.set_level('error') + + def scatter_nd_ref(self, input_data, indices, updates): + input_data[tuple(indices.t())] = updates.view(-1) + return input_data + + @parameterized.expand( + list( + product([ + ([[-3.0, -2.0, -1.0, 10.0, -25.0] + ], [[0.0, 1.0, 2.0, -2.0, -1.0]]), + ], [([0, 0, 1], [1, 0, 3])], [([-1.0, 0])], + ['float16', 'float32', 'int32']))) + def test_scatter(self, input_data, indices, updates, dtype): + + torch_dtype = str_dtype_to_torch(dtype) + input_data = torch.tensor(input_data).cuda() + indices = torch.tensor(indices).int().cuda() + updates = torch.tensor(updates).cuda() + + input_data = input_data.to(torch_dtype) + updates = updates.to(torch_dtype) + + # construct trt network + builder = tensorrt_llm.Builder() + network = builder.create_network() + with tensorrt_llm.net_guard(network): + input_t = Tensor(name='input', + shape=input_data.shape, + dtype=tensorrt_llm.str_dtype_to_trt(dtype)) + indices_t = Tensor(name='indices', + shape=indices.shape, + dtype=tensorrt_llm.str_dtype_to_trt('int32')) + updates_t = Tensor(name='updates', + shape=updates.shape, + dtype=tensorrt_llm.str_dtype_to_trt(dtype)) + + output = tensorrt_llm.functional.scatter_nd(input_t, indices_t, + updates_t) + + output.mark_output('output') + + # trt run + session = create_session(builder, network, precision=dtype) + inputs = {'input': input_data, 'indices': indices, 'updates': updates} + outputs = run_session(session, inputs) + + # pytorch run + ref = self.scatter_nd_ref(input_data, indices, updates) + + # compare diff + torch.testing.assert_close(ref, outputs['output']) diff --git a/tests/hlapi/apps/README.md b/tests/hlapi/apps/README.md new file mode 100644 index 000000000..8a0014f97 --- /dev/null +++ b/tests/hlapi/apps/README.md @@ -0,0 +1,3 @@ +This directory contains the end-to-end tests for the HLAPI applications in `examples/apps`. + +These tests are triggered in the `test_e2e.py`. diff --git a/tests/hlapi/apps/_test_llm_chat.py b/tests/hlapi/apps/_test_llm_chat.py new file mode 100644 index 000000000..8860b12b0 --- /dev/null +++ b/tests/hlapi/apps/_test_llm_chat.py @@ -0,0 +1,33 @@ +import os +import sys + +import pytest + +sys.path.append( + os.path.join(os.path.dirname(__file__), "..", "..", "..", "examples", + "apps")) +from chat import LLM, AutoTokenizer, LlmConsole, SamplingParams + +sys.path.append(os.path.join(os.path.dirname(__file__), '..')) +from test_llm import llama_model_path + + +@pytest.fixture +def interactive_console(): + model_dir = llama_model_path + tokenizer = AutoTokenizer.from_pretrained(model_dir) + llm = LLM(model_dir) + + sampling_params = SamplingParams() + + return LlmConsole(llm, tokenizer, sampling_params) + + +def test_interactive_console(interactive_console): + console = interactive_console + console.runsource('A B C') + assert len(console.history) == 2 + assert console.history[0]['content'] == 'A B C' + assert console.history[0]['role'] == 'user' + assert console.history[1]['role'] == 'assistant' + assert console.history[1]['content'] # reply not empty diff --git a/tests/hlapi/apps/_test_llm_server.py b/tests/hlapi/apps/_test_llm_server.py new file mode 100644 index 000000000..b375b2009 --- /dev/null +++ b/tests/hlapi/apps/_test_llm_server.py @@ -0,0 +1,47 @@ +import os +import sys + +import pytest +from fastapi.testclient import TestClient + +sys.path.append( + os.path.join(os.path.dirname(__file__), "..", "..", "..", "examples", + "apps")) +from fastapi_server import LLM, KvCacheConfig, LlmServer, SamplingParams + +sys.path.append(os.path.join(os.path.dirname(__file__), '..')) +from test_llm import llama_model_path + + +@pytest.fixture +def client(): + llm = LLM(llama_model_path) + sampling_params = SamplingParams() + kv_cache_config = KvCacheConfig() + + app_instance = LlmServer(llm, sampling_params, kv_cache_config) + client = TestClient(app_instance.app) + return client + + +def test_generate(client): + response = client.post("/generate", json={"prompt": "A B C"}) + assert response.status_code == 200 + assert "D E F" in response.json()["text"] + print(response.json()) + + +def test_generate_streaming(client): + with client.stream("POST", + "/generate", + json={ + "prompt": "A B C", + "streaming": True + }) as response: + assert response.status_code == 200 + chunks = [] + for chunk in response.iter_text(): + chunks.append(chunk) + + whole_text = "".join(chunks) + assert "D E F" in whole_text diff --git a/tests/hlapi/grid_searcher.py b/tests/hlapi/grid_searcher.py index fb9cd75ed..baa096b9c 100644 --- a/tests/hlapi/grid_searcher.py +++ b/tests/hlapi/grid_searcher.py @@ -1,12 +1,14 @@ #!/usr/bin/env python +import copy import operator import time from functools import reduce from pathlib import Path from typing import Any, Dict, Iterable, List, Optional -from tensorrt_llm import ModelConfig, logger -from tensorrt_llm.hlapi import CapacitySchedulerPolicy, KvCacheConfig +from tensorrt_llm import logger +from tensorrt_llm.hlapi import (BuildConfig, CapacitySchedulerPolicy, + KvCacheConfig, SchedulerConfig) from tensorrt_llm.hlapi._perf_evaluator import LLMPerfEvaluator from tensorrt_llm.hlapi.utils import print_colored @@ -15,32 +17,31 @@ class GridSearcher: ''' Test all the combinations of the options for the LLM(). Just for experimenting, not for production use. ''' - class Config: - model_config: ModelConfig - llm_kwargs: Dict[str, Any] - def __init__(self, prune_space_for_debug: int = 1e8): self.prune_space_for_debug = prune_space_for_debug self.latest_latency_per_case: Optional[float] = None def evaluate(self, - model_config: ModelConfig, + model: str, samples_path: Path, report_dir: Path, - specified_configs: Optional[List[Config]] = None, + specified_configs: Optional[List[dict]] = None, num_samples: int = -1, skip_steps=0, skip_configs: Optional[List[dict]] = None, - memory_monitor_interval: Optional[int] = None): + memory_monitor_interval: Optional[int] = None, + **kwargs): # Most of the knobs are referenced from docs/source/performance/perf-best-practices.md if not report_dir.exists(): report_dir.mkdir(parents=True, exist_ok=True) skip_configs = set([tuple(d.items()) for d in (skip_configs or [])]) - self.model_config = model_config space = specified_configs or self.generate_cases(self.tunable_space) - print_colored("Tunable options: ", color="green") + origin_build_config = kwargs.pop('build_config', BuildConfig()) + origin_kv_cache_config = kwargs.pop('kv_cache_config', KvCacheConfig()) + + print_colored("Tunable options: \n", color="green") for key, value in self.tunable_space.items(): print_colored(f" - {key}: {value}\n", color="green") print_colored("\n") @@ -68,8 +69,15 @@ def capacity_scheduling_policy_str(policy: CapacitySchedulerPolicy): "capacity_scheduling_policy"] = capacity_scheduling_policy_str( origin_llm_kwargs["capacity_scheduling_policy"]) - kvcache = KvCacheConfig() - kvcache.enable_block_reuse = llm_kwargs.pop('kvcache_reuse_blocks') + build_config = copy.deepcopy(origin_build_config) + kv_cache_config = copy.deepcopy(origin_kv_cache_config) + + build_config.plugin_config.multi_block_mode = llm_kwargs.pop( + 'multi_block_mode') + kv_cache_config.enable_block_reuse = llm_kwargs.pop( + 'kvcache_reuse_blocks') + scheduler_config = SchedulerConfig( + llm_kwargs.pop('capacity_scheduling_policy')) print_colored(f"Testing ", color="green") print_colored(f"{no}/{self.space_size}", color="bold_red") @@ -81,12 +89,15 @@ def capacity_scheduling_policy_str(policy: CapacitySchedulerPolicy): _start_time = time.time() with LLMPerfEvaluator.create( - model_config, + model, samples_path, num_samples=num_samples, warmup=max(num_samples // 10, 10), - kv_cache_config=kvcache, memory_monitor_interval=memory_monitor_interval, + build_config=build_config, + kv_cache_config=kv_cache_config, + scheduler_config=scheduler_config, + **kwargs, **llm_kwargs) as perf_evaluator: report_path = report_dir / f"report_{no}.json" @@ -109,9 +120,6 @@ def tunable_space(self): ], enable_chunked_context=[False, True], ) - if self.model_config.parallel_config.is_multi_gpu: - tunable_options["use_custom_all_reduce"] = [False, True] - self.space_size = reduce(operator.mul, [len(v) for v in tunable_options.values()], 1) self.space_size = min(self.space_size, self.prune_space_for_debug) diff --git a/tests/hlapi/hlapi_evaluator.py b/tests/hlapi/hlapi_evaluator.py index ca3dda8d9..d4141feb2 100644 --- a/tests/hlapi/hlapi_evaluator.py +++ b/tests/hlapi/hlapi_evaluator.py @@ -6,9 +6,10 @@ import click -from tensorrt_llm.hlapi import ModelConfig +from tensorrt_llm.hlapi import BuildConfig from tensorrt_llm.hlapi._perf_evaluator import LLMPerfEvaluator -from tensorrt_llm.hlapi.llm import ModelLoader, _ModelFormatKind +from tensorrt_llm.hlapi.llm import ModelLoader +from tensorrt_llm.hlapi.llm_utils import _ModelFormatKind from tensorrt_llm.hlapi.utils import print_colored try: @@ -31,7 +32,7 @@ def cli(): @click.option("--warmup", type=int, default=100, show_default=True) @click.option("--max-num-tokens", type=int, default=2048, show_default=True) @click.option("--max-input-length", type=int, required=True, default=200) -@click.option("--max-output-length", type=int, required=True, default=200) +@click.option("--max-seq-length", type=int, required=True, default=400) @click.option("--max-batch-size", type=int, default=128) @click.option("--engine-output-dir", type=str, default="") @click.option( @@ -40,7 +41,6 @@ def cli(): default=None, help="Path to the cpp executable, set it if you want to run the cpp benchmark" ) -@click.option("--enable-executor", is_flag=True, default=False) def benchmark_main(model_path: str, samples_path: str, report_path_prefix: str, @@ -52,8 +52,7 @@ def benchmark_main(model_path: str, max_seq_length: int = 400, max_batch_size: int = 128, engine_output_dir: str = "", - cpp_executable: str = None, - enable_executor: bool = False): + cpp_executable: str = None): ''' Run the benchmark on HLAPI. If `cpp_executable_path` is provided, it will run the cpp benchmark as well. ''' @@ -79,26 +78,21 @@ def benchmark_main(model_path: str, def run_hlapi(): print_colored(f"Running HLAPI benchmark ...\n", "bold_green") - config = ModelConfig(model_path) - build_config = config.build_config - build_config.max_num_tokens = max_num_tokens - build_config.max_input_len = max_input_length - build_config.max_seq_len = max_seq_length - build_config.max_batch_size = max_batch_size - config.parallel_config.tp_size = tp_size + build_config = BuildConfig(max_num_tokens=max_num_tokens, + max_input_len=max_input_length, + max_seq_len=max_seq_length, + max_batch_size=max_batch_size) evaluator = LLMPerfEvaluator.create( - config, + model=model_path, num_samples=num_samples, samples_path=samples_path, warmup=warmup, engine_cache_path=engine_output_dir, # The options should be identical to the cpp benchmark - use_custom_all_reduce=True, enable_chunked_context=False, - # additional options to LLM - enable_executor=enable_executor, - ) + tensor_parallel_size=tp_size, + build_config=build_config) assert evaluator report = evaluator.run() report.display() @@ -116,16 +110,17 @@ def run_hlapi(): def run_gpt_manager_benchmark(): print_colored(f"Running gptManagerBenchmark ...\n", "bold_green") - cpp_executable_path = ( - cpp_executable and cpp_executable != "on") or os.path.join( + if os.path.isfile(cpp_executable): + cpp_executable_path = cpp_executable + else: + cpp_executable_path = os.path.join( os.path.dirname(__file__), "../../cpp/build/benchmarks/gptManagerBenchmark") - run_command = f"{cpp_executable_path} --engine_dir {engine_output_dir} --type IFB --dataset {samples_path} --warm_up {warmup} --output_csv {report_path_prefix}.cpp.csv" - if enable_executor: - run_command += " --api executor" - launch_prefix = f"mpirun -n {tp_size}" if tp_size > 1 else "" - command = f"{launch_prefix} {run_command}" + command = f"{cpp_executable_path} --engine_dir {engine_output_dir} --type IFB --dataset {samples_path} --warm_up {warmup} --output_csv {report_path_prefix}.cpp.csv --api executor" + if tp_size > 1: + command = f"mpirun -n {tp_size} {command}" + print_colored(f'cpp benchmark command: {command}\n', "grey") output = subprocess.run(command, check=True, universal_newlines=True, @@ -133,7 +128,8 @@ def run_gpt_manager_benchmark(): capture_output=True, env=os.environ) # nosec B603 print_colored(f'cpp benchmark output: {output.stdout}', "grey") - print(f'cpp benchmark error: {output.stderr}', "red") + if output.stderr: + print_colored(f'cpp benchmark error: {output.stderr}', "red") run_hlapi() if cpp_executable: @@ -164,22 +160,19 @@ def grid_searcher_main(model_path, num_samples: int = 200): reports_root = Path(reports_root) - grid_searcher = GridSearcher(prune_space_for_debug=prune_space_for_debug, ) - - model_config = ModelConfig(model_path) - model_config.parallel_config.tp_size = tp_size + grid_searcher = GridSearcher(prune_space_for_debug=prune_space_for_debug) - model_config._set_additional_options(max_seq_len=max_seq_len, - max_input_len=max_input_len, - max_num_tokens=max_num_tokens) + build_config = BuildConfig(max_seq_len=max_seq_len, + max_input_len=max_input_len, + max_num_tokens=max_num_tokens) - grid_searcher.evaluate( - model_config=model_config, - samples_path=samples_path, - report_dir=reports_root, - memory_monitor_interval=1, - num_samples=num_samples, - ) + grid_searcher.evaluate(model=model_path, + samples_path=samples_path, + report_dir=reports_root, + memory_monitor_interval=1, + num_samples=num_samples, + tensor_parallel_size=tp_size, + build_config=build_config) if __name__ == '__main__': diff --git a/tests/hlapi/run_llm.py b/tests/hlapi/run_llm.py index 1cb20ecbc..ef04121ef 100644 --- a/tests/hlapi/run_llm.py +++ b/tests/hlapi/run_llm.py @@ -3,7 +3,7 @@ import click -from tensorrt_llm.hlapi import LLM, ModelConfig, SamplingParams +from tensorrt_llm.hlapi import LLM, SamplingParams @click.command() @@ -12,10 +12,7 @@ @click.option("--engine_dir", type=str, default=None) @click.option("--prompt", type=str, default=None) def main(model_dir: str, tp_size: int, engine_dir: str, prompt: str): - config = ModelConfig(model_dir) - config.parallel_config.tp_size = tp_size - - llm = LLM(config) + llm = LLM(model_dir, tensor_parallel_size=tp_size) if engine_dir is not None and os.path.abspath( engine_dir) != os.path.abspath(model_dir): diff --git a/tests/hlapi/test_build_cache.py b/tests/hlapi/test_build_cache.py new file mode 100644 index 000000000..8ad02342d --- /dev/null +++ b/tests/hlapi/test_build_cache.py @@ -0,0 +1,125 @@ +import json +from tempfile import TemporaryDirectory + +from tensorrt_llm.builder import EngineConfig +from tensorrt_llm.hlapi.build_cache import * +from tensorrt_llm.hlapi.llm_utils import CachedModelLoader, LlmArgs, ModelLoader + +try: + from test_llm import llama_model_path +except ImportError: + from .test_llm import llama_model_path + + +def test_BuildStep(): + with TemporaryDirectory() as tempdir: + build_cache = BuildCache(cache_root=Path(tempdir)) + build_step = build_cache.get_engine_building_cache_stage( + build_config=BuildConfig(), hf_model_name="test") + assert not build_step.cache_hitted() + print(build_step.get_cache_path().absolute()) + assert build_step.get_cache_metadata( + )["version"] == BuildCache.CACHE_VERSION + + with build_step.write_guard() as product_path: + product_path.mkdir() + with open(product_path / 'config.json', 'w') as f: + f.write(json.dumps({"a": 1, "b": 2})) + + assert build_step.cache_hitted() + + +def test_BuildCache_clean_untracked_path(): + # The BuildCache could cleanup the untracked files/dirs within the cache_root + with TemporaryDirectory() as tempdir: + build_cache = BuildCache(cache_root=Path(tempdir)) + (build_cache.cache_root / 'untracked').mkdir() + (build_cache.cache_root / 'untracked_file').touch() + + build_cache.prune_caches() + assert not (build_cache.cache_root / 'untracked').exists() + + +def test_BuildCache_clean_cache_exceed_record_limit(): + # The BuildCache could cleanup the cache if the number of records exceed the limit + with TemporaryDirectory() as tempdir: + build_cache = BuildCache(cache_root=Path(tempdir), max_records=2) + build_config = BuildConfig() + + def create_cache(hf_model_name: str): + step = build_cache.get_engine_building_cache_stage( + build_config=build_config, hf_model_name=hf_model_name) + assert not step.cache_hitted() + with step.write_guard() as product_path: + product_path.mkdir() + with open(product_path / 'config.json', 'w') as f: + f.write(json.dumps({"a": 1, "b": 2})) + + records = set() + + for i in range(3): + create_cache(str(i)) + for record in build_cache.load_cache_records(): + records.add(record) + + assert len(records) == 3 + + records = sorted(records, key=lambda x: x.time) + print(records) + + print(f"cache structure: {list(build_cache.cache_root.glob('*'))}") + assert len(list(build_cache.cache_root.glob('*'))) == 2 + + final_records = build_cache.load_cache_records() + # The earliest one should be pruned + assert records[0] not in final_records + for record in final_records: + assert record in records + + +def test_build_cache_prune_untracked_files(): + # The BuildCache could cleanup the untracked files/dirs within the cache_root + # The broken cache such as empty cache record directory should be pruned as well + with TemporaryDirectory() as tempdir: + build_cache = BuildCache(cache_root=Path(tempdir)) + (build_cache.cache_root / 'untracked').mkdir() + (build_cache.cache_root / 'untracked_file').touch() + (build_cache.cache_root / 'broken_cache').mkdir() + + build_cache.prune_caches() + assert not (build_cache.cache_root / 'untracked').exists() + assert not (build_cache.cache_root / 'untracked_file').exists() + assert not (build_cache.cache_root / 'broken_cache').exists() + + +def test_build_get_updated_build_cache(): + # Test the build method in commands/build.py get an updated BuildConfig that is the same as the real one in engine + # directory + build_config = BuildConfig() + build_config.max_batch_size = 100 + build_config.max_beam_width = 4 + + args = LlmArgs(model=llama_model_path, + build_config=build_config, + enable_tqdm=True) + args.setup() + ml = ModelLoader(args, tokenizer=None) + + with TemporaryDirectory() as engine_dir: + engine_dir = ml(Path(engine_dir)) + + updated_build_config = CachedModelLoader.get_final_build_config( + args, Path(llama_model_path)) + + actual_build_config = EngineConfig.from_json_file( + engine_dir / 'config.json').build_config + + assert BuildCache.prune_build_config_for_cache_key( + updated_build_config.to_dict( + )) == BuildCache.prune_build_config_for_cache_key( + actual_build_config.to_dict()) + + +if __name__ == '__main__': + #test_build_get_updated_build_cache() + test_build_cache_prune_untracked_files() diff --git a/tests/hlapi/test_executor.py b/tests/hlapi/test_executor.py index b5cb75f95..b491df61a 100644 --- a/tests/hlapi/test_executor.py +++ b/tests/hlapi/test_executor.py @@ -7,13 +7,13 @@ import pytest import torch -from transformers import AutoTokenizer from tensorrt_llm._utils import mpi_world_size from tensorrt_llm.bindings import executor as tllm from tensorrt_llm.executor import (GenerationExecutor, GenerationRequest, SamplingParams) -from tensorrt_llm.hlapi.llm import LLM, ModelConfig +from tensorrt_llm.hlapi import LLM, BuildConfig +from tensorrt_llm.hlapi.tokenizer import TransformersTokenizer _sys.path.append(_os.path.join(_os.path.dirname(__file__), '..')) from utils.cpp_paths import * # noqa @@ -28,9 +28,8 @@ def llama_7b_path(engine_path: Path) -> Path: path = engine_path / "llama7b" if not path.exists(): - config = ModelConfig(str(llm_models_root() / - "llama-models/llama-7b-hf")) - llm = LLM(config) + model_dir = str(llm_models_root() / "llama-models/llama-7b-hf") + llm = LLM(model_dir) llm.save(str(path)) return path @@ -41,11 +40,11 @@ def llama_7b_bs2_path(engine_path: Path) -> Path: path = engine_path / "llama7b_bs2" if not path.exists(): - config = ModelConfig(str(llm_models_root() / - "llama-models/llama-7b-hf")) - config.build_config.max_beam_width = 2 + model_dir = str(llm_models_root() / "llama-models/llama-7b-hf") + build_config = BuildConfig() + build_config.max_beam_width = 2 # TODO[chunweiy]: switch to executor backend - llm = LLM(config) + llm = LLM(model_dir, build_config=build_config) llm.save(str(path)) return path @@ -56,10 +55,8 @@ def llama_7b_tp2_path(engine_path: Path) -> Path: path = engine_path / "llama7b-tp2" if not path.exists(): - config = ModelConfig(str(llm_models_root() / - "llama-models/llama-7b-hf")) - config.parallel_config.tp_size = 2 - llm = LLM(config) + model_dir = str(llm_models_root() / "llama-models/llama-7b-hf") + llm = LLM(model_dir, tensor_parallel_size=2) llm.save(str(path)) return path @@ -67,112 +64,120 @@ def llama_7b_tp2_path(engine_path: Path) -> Path: @pytest.mark.skipif(WORLD_SIZE != 1, reason="Must run on single MPI rank") def test_generation_bs2(llama_7b_bs2_path: Path): - tokenizer = llama_7b_bs2_path + tokenizer = TransformersTokenizer.from_pretrained(llama_7b_bs2_path) prompt = "A B C D" + prompt_token_ids = tokenizer.encode(prompt) max_new_tokens = 8 with GenerationExecutor.create( llama_7b_bs2_path, - tokenizer, executor_config=tllm.ExecutorConfig(max_beam_width=2)) as executor: - result = executor.generate(prompt, + result = executor.generate(prompt_token_ids, sampling_params=SamplingParams( max_new_tokens=max_new_tokens, beam_width=2)) - assert similar(result.text[0], 'E F G H I J K L') - assert similar(result.text[1], 'E F G H I K L M') + assert similar(tokenizer.decode(result.outputs[0].token_ids), + 'E F G H I J K L') + assert similar(tokenizer.decode(result.outputs[1].token_ids), + 'E F G H I K L M') @pytest.mark.skipif(WORLD_SIZE != 1, reason="Must run on single MPI rank") def test_sync_generation(llama_7b_path: Path): - tokenizer = llama_7b_path + tokenizer = TransformersTokenizer.from_pretrained(llama_7b_path) prompt = "A B C D" + prompt_token_ids = tokenizer.encode(prompt) + expected_output = "E F G H" expected_long_output = "E F G H I J K L" - split_output = ["E", " F", " G", " H", " I", " J", " K", " L"] sampling_params0 = SamplingParams(max_new_tokens=4) sampling_params1 = SamplingParams(max_new_tokens=8) - with GenerationExecutor.create(llama_7b_path, tokenizer) as executor: + with GenerationExecutor.create(llama_7b_path) as executor: # Simple generations (synchronous) - result = executor.generate(prompt, sampling_params=sampling_params0) - assert result.text == expected_output + result = executor.generate(prompt_token_ids, + sampling_params=sampling_params0) + assert tokenizer.decode(result.outputs[0].token_ids) == expected_output results = executor.generate( - [prompt, prompt], + [prompt_token_ids, prompt_token_ids], sampling_params=[sampling_params0, sampling_params1]) for result, expected in zip(results, (expected_output, expected_long_output)): - assert result.text == expected + assert tokenizer.decode(result.outputs[0].token_ids) == expected - # Simple generations (asynchronous) - # # Iterate the partial results when streaming - future = executor.generate_async(prompt, - streaming=True, - sampling_params=sampling_params0) - for idx, partial_result in enumerate(future): - assert partial_result.text_diff == split_output[idx] + future = executor.generate_async(prompt_token_ids, + sampling_params=sampling_params0, + streaming=True) + for partial_result in future: + partial_text = tokenizer.decode(partial_result.outputs[0].token_ids) + assert expected_output.startswith(partial_text) # Iterate the partial results when streaming # Streaming results in nested loop - futures = executor.generate_async( - [prompt, prompt], - streaming=True, - sampling_params=[sampling_params0, sampling_params1]) - for future in futures: - for idx, partial_result in enumerate(future): - assert partial_result.text_diff == split_output[idx] + for sampling_params in [sampling_params0, sampling_params1]: + future = executor.generate_async(prompt_token_ids, + sampling_params=sampling_params, + streaming=True) + for partial_result in future: + partial_text = tokenizer.decode( + partial_result.outputs[0].token_ids) + assert expected_long_output.startswith(partial_text) # Low-level api with .submit # Submit a batch of requests - tokenizer = AutoTokenizer.from_pretrained("gpt2") futures = [] for _ in range(5): futures.append( executor.submit( - GenerationRequest( - prompt, - tokenizer=AutoTokenizer.from_pretrained(llama_7b_path), - sampling_params=sampling_params0))) + GenerationRequest(prompt_token_ids, + sampling_params=sampling_params0))) for future in executor.wait_first_completed(futures): assert future.done - assert future.result().text == "".join(split_output[:4]) + assert tokenizer.decode( + future.result().outputs[0].token_ids) == expected_output @pytest.mark.skipif(torch.cuda.device_count() < 2 or WORLD_SIZE != 2, reason="Must run on 2 MPI ranks with at least 2 GPUs") def test_sync_generation_tp_main_node_only(llama_7b_tp2_path: Path): + tokenizer = TransformersTokenizer.from_pretrained(llama_7b_tp2_path) prompt = "deep learning" + prompt_token_ids = tokenizer.encode(prompt) sampling_params = SamplingParams(max_new_tokens=4) - with GenerationExecutor.create(llama_7b_tp2_path, - llama_7b_tp2_path) as executor: + with GenerationExecutor.create(llama_7b_tp2_path) as executor: executor.block_subordinates() # from now on, only rank0 lives in the with statement # other nodes wait at the "end" of the with statement - result = executor.generate(prompt, sampling_params=sampling_params) - assert result.text == " deep learning, neural network," + result = executor.generate(prompt_token_ids, + sampling_params=sampling_params) + assert tokenizer.decode( + result.outputs[0].token_ids) == " deep learning, neural network," @pytest.mark.skipif(torch.cuda.device_count() < 2 or WORLD_SIZE != 1, reason="Must run on 1 MPI rank with at least 2 GPUs") def test_sync_generation_tp_inner(llama_7b_tp2_path: Path): + tokenizer = TransformersTokenizer.from_pretrained(llama_7b_tp2_path) prompt = "deep learning" + prompt_token_ids = tokenizer.encode(prompt) tp_size = 2 sampling_params = SamplingParams(max_new_tokens=4) executor = GenerationExecutor.create(llama_7b_tp2_path, - llama_7b_tp2_path, model_world_size=tp_size) async def async_stats_task(): # asyncio event loop must be created before first generation in order to # use async APIs. - result = executor.generate(prompt, sampling_params=sampling_params) - assert result.text == ", neural network," + result = executor.generate(prompt_token_ids, + sampling_params=sampling_params) + assert tokenizer.decode( + result.outputs[0].token_ids) == ", neural network," stats = await executor.aget_stats() stats = json.loads(stats) diff --git a/tests/hlapi/test_llm.py b/tests/hlapi/test_llm.py index 02001f7ae..a3bb12775 100644 --- a/tests/hlapi/test_llm.py +++ b/tests/hlapi/test_llm.py @@ -9,12 +9,11 @@ import torch from transformers import AutoTokenizer -from tensorrt_llm.hlapi.llm import (LLM, BuildConfig, KvCacheConfig, - ModelConfig, ParallelConfig, - PretrainedConfig, SamplingParams, - StreamingLLMParam, TokenizerBase) +from tensorrt_llm.hlapi import LLM, KvCacheConfig, SamplingParams, TokenizerBase +from tensorrt_llm.hlapi.llm_utils import BuildConfig, _ParallelConfig from tensorrt_llm.hlapi.tokenizer import TransformersTokenizer -from tensorrt_llm.hlapi.utils import get_total_gpu_memory +from tensorrt_llm.hlapi.utils import GpuArch, get_total_gpu_memory +from tensorrt_llm.models import PretrainedConfig sys.path.append(os.path.join(os.path.dirname(__file__), '..')) from utils.llm_data import llm_models_root @@ -23,7 +22,8 @@ from tensorrt_llm.models.llama.model import LLaMAForCausalLM # The unittests are based on the tiny-llama, which is fast to build and run. -# There are other tests based on llama-7B model, such as the end-to-end tests in test_e2e.py, and parallel tests in test_llm_multi_gpu.py. +# There are other tests based on llama-7B model, such as the end-to-end tests in test_e2e.py, and parallel tests in +# test_llm_multi_gpu.py. def get_model_path(model_name): @@ -42,19 +42,18 @@ def get_model_path(model_name): @force_ampere -def test_ModelConfig_build_config(): - config = ModelConfig(llama_model_path) - assert config.build_config is not None - +def test_llm_build_config(): + build_config = BuildConfig() # change some building parameters - config.build_config.max_batch_size = 129 - config.build_config.max_beam_width = 4 - config.build_config.builder_opt = 3 - config.build_config.max_num_tokens = 888 - config.build_config.strongly_typed = True - config.build_config.max_seq_len = 1024 - - llm = LLM(config, + build_config.max_batch_size = 129 + build_config.max_beam_width = 4 + build_config.builder_opt = 3 + build_config.max_num_tokens = 888 + build_config.strongly_typed = True + build_config.max_seq_len = 333 + + llm = LLM(model=llama_model_path, + build_config=build_config, kv_cache_config=KvCacheConfig(free_gpu_memory_fraction=0.4)) tmpdir = tempfile.TemporaryDirectory() llm.save(tmpdir.name) @@ -65,33 +64,34 @@ def test_ModelConfig_build_config(): pretrained_config = PretrainedConfig.from_dict( engine_config["pretrained_config"]) - build_config = BuildConfig.from_dict(engine_config["build_config"]) + build_config1 = BuildConfig.from_dict(engine_config["build_config"]) # Know issue: this will be converted to None after save engine for single-gpu - build_config.plugin_config.nccl_plugin = 'float16' - assert build_config.max_batch_size == config.build_config.max_batch_size - assert build_config.max_beam_width == config.build_config.max_beam_width - assert build_config.builder_opt == config.build_config.builder_opt - assert build_config.max_num_tokens == config.build_config.max_num_tokens - assert build_config.strongly_typed == config.build_config.strongly_typed - assert build_config.max_seq_len == config.build_config.max_seq_len + build_config1.plugin_config.nccl_plugin = 'float16' + assert build_config1.max_batch_size == build_config.max_batch_size + assert build_config1.max_beam_width == build_config.max_beam_width + assert build_config1.builder_opt == build_config.builder_opt + assert build_config1.max_num_tokens == build_config.max_num_tokens + assert build_config1.strongly_typed == build_config.strongly_typed + assert build_config1.max_seq_len == build_config.max_seq_len def test_llm_loading_from_hf(): - config = ModelConfig(llama_model_path) - # The performance-related flags are turned on eagerly to check the functionality - llm = LLM( - config, + model=llama_model_path, kv_cache_config=KvCacheConfig(free_gpu_memory_fraction=0.4), - enable_chunked_context=False, + enable_chunked_context=True, ) + if GpuArch.is_post_ampere(): + # chunked_context should be enabled by default + assert llm.args.enable_chunked_context + sampling_params = SamplingParams(max_new_tokens=8) for output in llm.generate(prompts, sampling_params=sampling_params): print(output) - assert output.text == "D E F G H I J K" + assert output.outputs[0].text == "D E F G H I J K" @force_ampere @@ -103,9 +103,8 @@ def test_llm_loading_from_ckpt(): llama.save_checkpoint(ckpt_dir) del llama - config = ModelConfig(ckpt_dir) llm = LLM( - config, + model=ckpt_dir, tokenizer=tokenizer, kv_cache_config=KvCacheConfig(free_gpu_memory_fraction=0.4), ) @@ -114,7 +113,7 @@ def test_llm_loading_from_ckpt(): for output in llm.generate(prompts, sampling_params=sampling_params): print(output) - assert output.text == "D E F G H I J K" + assert output.outputs[0].text == "D E F G H I J K" class MyTokenizer(TokenizerBase): @@ -149,11 +148,10 @@ def batch_encode_plus(self, texts: List[str], **kwargs) -> dict: def test_llm_with_customized_tokenizer(): - config = ModelConfig(llama_model_path) llm = LLM( - config, + model=llama_model_path, # a customized tokenizer is passed to override the default one - tokenizer=MyTokenizer.from_pretrained(config.model_dir), + tokenizer=MyTokenizer.from_pretrained(llama_model_path), kv_cache_config=KvCacheConfig(free_gpu_memory_fraction=0.4), ) @@ -162,10 +160,11 @@ def test_llm_with_customized_tokenizer(): def test_llm_without_tokenizer(): - config = ModelConfig(llama_model_path) llm = LLM( - config, + model=llama_model_path, + skip_tokenizer_init=True, kv_cache_config=KvCacheConfig(free_gpu_memory_fraction=0.4), + enable_chunked_context=True, ) sampling_params = SamplingParams(end_id=2, pad_id=2, max_new_tokens=8) @@ -173,7 +172,8 @@ def test_llm_without_tokenizer(): prompts = [[23, 14, 3]] for output in llm.generate(prompts, sampling_params=sampling_params): - assert not output.text, "The output should be empty since the tokenizer is missing" + assert not output.outputs[0].text, \ + "The output should be empty since the tokenizer is missing" print(output) @@ -200,24 +200,19 @@ def _test_llm_generate_async(model_name=default_model_name, if "Mixtral" in model_name and use_auto_parallel: pytest.skip("Auto parallel is not supported for Mixtral models") - config = ModelConfig(llama_model_path) - if use_auto_parallel: - config.parallel_config.auto_parallel = True - config.parallel_config.world_size = tp_size - else: - config.parallel_config.tp_size = tp_size - kv_cache_config = KvCacheConfig(free_gpu_memory_fraction=0.4) - devices = config.parallel_config.devices - if torch.cuda.get_device_properties(devices[0]).major >= 8: - kv_cache_config.enable_block_reuse = True + + tp_size = tp_size if not use_auto_parallel else 1 + world_size = tp_size if use_auto_parallel else None llm = LLM( - config, + model=get_model_path(model_name), tokenizer=tokenizer, kv_cache_config=kv_cache_config, + tensor_parallel_size=tp_size, + auto_parallel=use_auto_parallel, + world_size=world_size, ) - llm.save("./tmp.engine.8") # DEBUG sampling_params = SamplingParams(max_new_tokens=6) @@ -229,7 +224,7 @@ async def task(prompt: str): prompt, streaming=streaming, sampling_params=sampling_params): print('output', output) - outputs.append(output.text) + outputs.append(output.outputs[0].text) print(' '.join(outputs)) async def main(): @@ -251,7 +246,7 @@ def test_non_streaming_usage_wait(): output = llm.generate_async(prompt, streaming=False, sampling_params=sampling_params) - print(output.text) + print(output.outputs[0].text) def test_future(streaming: bool): for prompt in prompts: @@ -262,11 +257,11 @@ def test_future(streaming: bool): for output in future: # Do something else and then wait for the result if needed output = output.result(timeout=10) - print('future', output.text) + print('future', output.outputs[0].text) else: # Do something else and then wait for the result if needed output = future.result(timeout=10) - print('future', output.text) + print('future', output.outputs[0].text) def test_future_async(): @@ -275,7 +270,7 @@ async def task(prompt: str): streaming=False, sampling_params=sampling_params) output = await future.aresult() - print('future', output.text) + print('future', output.outputs[0].text) async def main(): tasks = [task(prompt) for prompt in prompts] @@ -295,9 +290,8 @@ async def main(): @pytest.fixture(scope="module") def llm_for_sampling_params() -> LLM: - config = ModelConfig(llama_model_path) llm = LLM( - config, + model=llama_model_path, kv_cache_config=KvCacheConfig(free_gpu_memory_fraction=0.4), ) return llm @@ -314,7 +308,7 @@ def test_generate_with_sampling_params_per_prompt(llm_for_sampling_params: LLM): for i, output in enumerate( llm.generate(prompts, sampling_params=sampling_params_list)): - output_len = len(output.token_ids) + output_len = len(output.outputs[0].token_ids) print(f"output_len: {output_len}") assert output_len <= sampling_params_list[i].max_new_tokens @@ -350,12 +344,13 @@ def test_generate_with_SamplingConfig(llm_for_sampling_params: LLM, @force_ampere def test_generate_with_beam_search(): - config = ModelConfig(llama_model_path) - config.build_config.max_beam_width = 2 - config.build_config.max_num_tokens = 20 + build_config = BuildConfig() + build_config.max_beam_width = 2 + build_config.max_num_tokens = 20 llm = LLM( - config, + model=llama_model_path, + build_config=build_config, kv_cache_config=KvCacheConfig(free_gpu_memory_fraction=0.4), ) @@ -363,31 +358,36 @@ def test_generate_with_beam_search(): for output in llm.generate(prompts, sampling_params=sampling_params): print(output) - assert len(output.text) == 2 - assert len(output.token_ids) == 2 - assert similar(output.text[0], "D E F G H I") - assert similar(output.text[1], "D E F G I J") + assert len(output.outputs) == 2 + assert similar(output.outputs[0].text, "D E F G H I") + assert similar(output.outputs[1].text, "D E F G I J") @force_ampere def test_generate_with_streaming_llm(): - config = ModelConfig(llama_model_path) # TODO[chunweiy]: Test with larger size when the underlying support is ready - llm = LLM(config, streaming_llm=StreamingLLMParam(64, 4)) + build_config = BuildConfig() + build_config.plugin_config.streamingllm = True + kv_cache_config = KvCacheConfig(max_attention_window=64, + sink_token_length=4) + + llm = LLM(model=llama_model_path, + kv_cache_config=kv_cache_config, + build_config=build_config) # Check the plugin config is correctly set - assert config.build_config.plugin_config.streamingllm is True - assert config.build_config.plugin_config.use_paged_context_fmha is False + assert build_config.plugin_config.streamingllm is True + #assert build_config.plugin_config.use_paged_context_fmha is False sampling_params = SamplingParams(max_new_tokens=4) for output in llm.generate(prompts, sampling_params=sampling_params): - assert output.text == "D E F G" + assert output.outputs[0].text == "D E F G" print(output) def test_parallel_config(): - config = ParallelConfig() + config = _ParallelConfig() config.tp_size = 2 config.pp_size = 2 assert config.world_size == 4 @@ -407,14 +407,14 @@ def test_generate_with_OutputConfig(gather_context_logits: bool, if not (gather_context_logits or gather_generation_logits): # prune space return - config = ModelConfig(llama_model_path) - config.build_config.gather_context_logits = gather_context_logits - config.build_config.gather_generation_logits = gather_generation_logits - config.build_config.return_log_probs = return_log_probs + build_config = BuildConfig() + build_config.gather_context_logits = gather_context_logits + build_config.gather_generation_logits = gather_generation_logits llm = LLM( - config, + model=llama_model_path, kv_cache_config=KvCacheConfig(free_gpu_memory_fraction=0.4), + build_config=build_config, ) sampling_params = SamplingParams( max_new_tokens=8, @@ -427,20 +427,19 @@ def test_generate_with_OutputConfig(gather_context_logits: bool, assert output.context_logits is not None assert len(prompts[0].split()) + 1 == output.context_logits.shape[0] if gather_generation_logits: - assert output.generation_logits is not None - assert sampling_params.max_new_tokens == output.generation_logits.shape[ - 1] + assert output.outputs[0].generation_logits is not None + assert sampling_params.max_new_tokens == output.outputs[ + 0].generation_logits.shape[0] if return_log_probs: - assert output.log_probs is not None + assert output.outputs[0].logprobs is not None print(output) @force_ampere def test_generate_with_stop_words(): - config = ModelConfig(llama_model_path) llm = LLM( - config, + model=llama_model_path, kv_cache_config=KvCacheConfig(free_gpu_memory_fraction=0.4), ) @@ -448,14 +447,14 @@ def test_generate_with_stop_words(): for output in llm.generate(prompts, sampling_params=sampling_params): print(output) - assert output.text == "D E F G H I" + assert output.outputs[0].text == "D E F G H I" +@pytest.mark.skip("waive") @force_ampere def test_generate_with_bad_words(): - config = ModelConfig(llama_model_path) llm = LLM( - config, + model=llama_model_path, kv_cache_config=KvCacheConfig(free_gpu_memory_fraction=0.4), ) @@ -473,14 +472,14 @@ def test_generate_with_bad_words(): for output in llm.generate(prompts, sampling_params=sampling_params): print(output) - assert output.text == "D E F G H J" + assert output.outputs[0].text == "D E F G HI" +@pytest.mark.skip("waive") @force_ampere def test_generate_with_embedding_bias(): - config = ModelConfig(llama_model_path) llm = LLM( - config, + llama_model_path, kv_cache_config=KvCacheConfig(free_gpu_memory_fraction=0.4), ) @@ -497,9 +496,10 @@ def test_generate_with_embedding_bias(): for output in llm.generate(prompts, sampling_params=sampling_params): print(output) - assert output.text == "Z Z Z Z Z Z" + assert output.outputs[0].text == "Z Z Z Z Z Z" +@pytest.mark.skip("waive") @force_ampere def test_generate_with_logits_post_processor(): tokenizer = AutoTokenizer.from_pretrained(llama_model_path, @@ -512,8 +512,7 @@ def logits_post_processor(req_id: int, logits: torch.Tensor, logits[:] = float("-inf") logits[..., biased_word_id] = 0 - config = ModelConfig(llama_model_path) - llm = LLM(config, + llm = LLM(llama_model_path, kv_cache_config=KvCacheConfig(free_gpu_memory_fraction=0.4), logits_post_processor_map={"my_logits_pp": logits_post_processor}) @@ -522,21 +521,20 @@ def logits_post_processor(req_id: int, logits: torch.Tensor, for output in llm.generate(prompts, sampling_params=sampling_params): print(output) - assert output.text == "Z Z Z Z Z Z" + assert output.outputs[0].text == "Z Z Z Z Z Z" @force_ampere def test_generate_block_reuse(): - config = ModelConfig(llama_model_path) llm = LLM( - config, + model=llama_model_path, kv_cache_config=KvCacheConfig(free_gpu_memory_fraction=0.4, enable_block_reuse=True), ) # Check the configurations are correctly set - assert config.build_config.plugin_config.use_paged_context_fmha is True - assert config.build_config.plugin_config.paged_kv_cache is True + assert llm.args.build_config.plugin_config.use_paged_context_fmha is True + assert llm.args.build_config.plugin_config.paged_kv_cache is True sampling_params = SamplingParams(max_new_tokens=6) diff --git a/tests/hlapi/test_llm_download.py b/tests/hlapi/test_llm_download.py index feb39b9e6..5d16f57c8 100644 --- a/tests/hlapi/test_llm_download.py +++ b/tests/hlapi/test_llm_download.py @@ -1,17 +1,37 @@ -from tensorrt_llm.hlapi import LLM, ModelConfig -from tensorrt_llm.hlapi.utils import download_hf_model +from tensorrt_llm.hlapi import LLM +from tensorrt_llm.hlapi.utils import (download_hf_model, + download_hf_pretrained_config) -prompts = ["A B C"] +try: + from .test_llm import llama_model_path +except ImportError: + from .test_llm import llama_model_path +prompts = ["A B C"] -def test_download_hf_model(): - dir = download_hf_model("TinyLlama/TinyLlama-1.1B-Chat-v1.0") - assert dir.exists() - print(f"Downloaded model to {dir}") +model_name = "TinyLlama/TinyLlama-1.1B-Chat-v1.0" def test_llm_with_model_downloaded(): - config = ModelConfig(model="TinyLlama/TinyLlama-1.1B-Chat-v1.0") - llm = LLM(config) + llm = LLM(model=model_name, enable_build_cache=True) + for output in llm.generate(prompts): + print(output) + + +def test_llm_with_tokenizer_downloaded(): + llm = LLM(model=llama_model_path, tokenizer=model_name) for output in llm.generate(prompts): print(output) + + +def test_download_config(): + path0 = download_hf_pretrained_config(model_name) + print(f"download config to {path0}") + path1 = download_hf_model(model_name) + print(f"download model to {path1}") + assert path0 == path1 + + +if __name__ == "__main__": + test_download_config() + test_llm_with_model_downloaded() diff --git a/tests/hlapi/test_llm_multi_gpu.py b/tests/hlapi/test_llm_multi_gpu.py index ad289a453..240cec301 100644 --- a/tests/hlapi/test_llm_multi_gpu.py +++ b/tests/hlapi/test_llm_multi_gpu.py @@ -7,8 +7,8 @@ import torch from parameterized import parameterized -from tensorrt_llm.hlapi.llm import (LLM, KvCacheConfig, ModelConfig, - SamplingParams) +from tensorrt_llm.hlapi.llm import LLM, SamplingParams +from tensorrt_llm.hlapi.llm_utils import KvCacheConfig from tensorrt_llm.hlapi.tokenizer import TransformersTokenizer sys.path.append(os.path.join(os.path.dirname(__file__), '..')) @@ -45,13 +45,12 @@ def engine_from_checkpoint() -> tempfile.TemporaryDirectory: llama.save_checkpoint(ckpt_dir, save_config=(rank == 0)) del llama - config = ModelConfig(ckpt_dir) - assert config.parallel_config.tp_size == tp_size llm = LLM( - config, + ckpt_dir, tokenizer=tokenizer, kv_cache_config=KvCacheConfig(free_gpu_memory_fraction=0.4), ) + assert llm.args.parallel_config.tp_size == tp_size tmpdir = tempfile.TemporaryDirectory() llm.save(tmpdir.name) @@ -65,26 +64,26 @@ def engine_from_checkpoint() -> tempfile.TemporaryDirectory: def test_llm_loading_from_ckpt_for_tp2( engine_from_checkpoint: tempfile.TemporaryDirectory, enable_executor: bool): - config = ModelConfig(engine_from_checkpoint.name) tokenizer = TransformersTokenizer.from_pretrained(llama_model_path) - llm = LLM(config, tokenizer=tokenizer, enable_executor=enable_executor) + llm = LLM(engine_from_checkpoint.name, + tokenizer=tokenizer, + enable_executor=enable_executor) sampling_params = SamplingParams(max_new_tokens=8) for output in llm.generate(prompts, sampling_params=sampling_params): print(output) - assert output.text == "D E F G H I J K" + assert output.outputs[0].text == "D E F G H I J K" @skip_single_gpu def test_llm_generate_tp2(engine_from_checkpoint): model_dir = engine_from_checkpoint.name tokenizer = TransformersTokenizer.from_pretrained(llama_model_path) - config = ModelConfig(model_dir) - config.parallel_config.tp_size = 2 llm = LLM( - config, + model_dir, + tensor_parallel_size=2, tokenizer=tokenizer, kv_cache_config=KvCacheConfig(free_gpu_memory_fraction=0.4), ) @@ -131,10 +130,9 @@ def is_memory_enough_for_mixtral(): @pytest.mark.skipif(not is_memory_enough_for_mixtral(), reason="The test needs at least 160GB memory, skipping") def test_llm_generate_mixtral_for_tp2(): - config = ModelConfig(get_model_path(mixtral_model_name)) - config.parallel_config.tp_size = 2 llm = LLM( - config, + get_model_path(mixtral_model_name), + tensor_parallel_size=2, kv_cache_config=KvCacheConfig(free_gpu_memory_fraction=0.4), ) for output in llm.generate(prompts): @@ -142,17 +140,16 @@ def test_llm_generate_mixtral_for_tp2(): def test_llm_pp2(): - config = ModelConfig(llama_model_path) - config.parallel_config.pp_size = 2 - config.parallel_config.auto_parallel = False llm = LLM( - config, + llama_model_path, + pipeline_parallel_size=2, + auto_parallel=False, kv_cache_config=KvCacheConfig(free_gpu_memory_fraction=0.4), ) sampling_params = SamplingParams(max_new_tokens=8, beam_width=1) for output in llm.generate(prompts, sampling_params=sampling_params): print(output) - assert output.text == "D E F G H I J K" + assert output.outputs[0].text == "D E F G H I J K" def llm_end2end_tp2_cases(): @@ -169,35 +166,40 @@ def llm_end2end_tp2_cases(): @parameterized.expand(llm_end2end_tp2_cases(), name_func=unittest_name_func) def test_llm_end2end_tp2(llm_additional_options): model_path = get_model_path(default_model_name) - config = ModelConfig(model_path) - config.parallel_config.tp_size = 2 - llm = LLM(config, **llm_additional_options) + llm = LLM(model_path, tensor_parallel_size=2, **llm_additional_options) + assert llm.args._convert_checkpoint_options embedding_parallel_mode = llm_additional_options.pop( 'embedding_parallel_mode', 'SHARDING_ALONG_VOCAB') if embedding_parallel_mode == 'NONE': - assert llm._convert_checkpoint_options['use_parallel_embedding'] is False + assert llm.args._convert_checkpoint_options[ + 'use_parallel_embedding'] is False elif embedding_parallel_mode == 'SHARDING_ALONG_VOCAB': - assert llm._convert_checkpoint_options['use_parallel_embedding'] is True - assert llm._convert_checkpoint_options['embedding_sharding_dim'] == 0 + assert llm.args._convert_checkpoint_options[ + 'use_parallel_embedding'] is True + assert llm.args._convert_checkpoint_options[ + 'embedding_sharding_dim'] == 0 elif embedding_parallel_mode == 'SHARDING_ALONG_HIDDEN': - assert llm._convert_checkpoint_options['use_parallel_embedding'] is True - assert llm._convert_checkpoint_options['embedding_sharding_dim'] == 1 + assert llm.args._convert_checkpoint_options[ + 'use_parallel_embedding'] is True + assert llm.args._convert_checkpoint_options[ + 'embedding_sharding_dim'] == 1 if 'share_embedding_table' in llm_additional_options: - assert llm._convert_checkpoint_options[ + assert llm.args._convert_checkpoint_options[ 'share_embedding_table'] == llm_additional_options.pop( 'share_embedding_table') else: - assert llm._convert_checkpoint_options['share_embedding_table'] is False + assert llm.args._convert_checkpoint_options[ + 'share_embedding_table'] is False assert len(llm_additional_options) == 0 sampling_params = SamplingParams(max_new_tokens=8) for output in llm.generate(prompts, sampling_params=sampling_params): print(output) - assert output.text == "D E F G H I J K" + assert output.outputs[0].text == "D E F G H I J K" @skip_single_gpu diff --git a/tests/hlapi/test_llm_perf_evaluator.py b/tests/hlapi/test_llm_perf_evaluator.py index 87ebaaf63..6155e05c2 100644 --- a/tests/hlapi/test_llm_perf_evaluator.py +++ b/tests/hlapi/test_llm_perf_evaluator.py @@ -5,20 +5,17 @@ import time from pathlib import Path +from tensorrt_llm.hlapi import KvCacheConfig from tensorrt_llm.hlapi._perf_evaluator import (LLMPerfEvaluator, MemoryContinuousMonitorThread) -from tensorrt_llm.hlapi.llm import KvCacheConfig, ModelConfig sys.path.append(os.path.join(os.path.dirname(__file__), '..')) -from utils.llm_data import llm_models_root from utils.util import force_ampere - -def get_model_path(model_name): - return str(llm_models_root() / model_name) - - -llama_model_path = get_model_path("llama-models/llama-7b-hf") +try: + from test_llm import llama_model_path +except ImportError: + from .test_llm import llama_model_path def test_memory_thread(): @@ -45,8 +42,6 @@ def gen_fake_samples(samples_path: str, num_samples: int, sample_length: int): @force_ampere def test_perf_evaluator(): - config = ModelConfig(llama_model_path) - with tempfile.TemporaryDirectory() as temp_dir: workspace = Path(temp_dir) samples_path = workspace / "data.json" @@ -56,7 +51,7 @@ def test_perf_evaluator(): kvcache_config = KvCacheConfig(enable_block_reuse=True) evaluator = LLMPerfEvaluator.create( - config, + model=llama_model_path, num_samples=10, samples_path=samples_path, warmup=10, diff --git a/tests/hlapi/test_llm_quant.py b/tests/hlapi/test_llm_quant.py index 6e7014dcb..edf99b9f2 100644 --- a/tests/hlapi/test_llm_quant.py +++ b/tests/hlapi/test_llm_quant.py @@ -1,8 +1,8 @@ import os import sys -from tensorrt_llm.hlapi.llm import LLM, ModelConfig, SamplingParams -from tensorrt_llm.quantization import QuantAlgo +from tensorrt_llm.hlapi.llm import LLM, SamplingParams +from tensorrt_llm.hlapi.llm_utils import QuantAlgo, QuantConfig sys.path.append(os.path.join(os.path.dirname(__file__), '..')) from utils.util import skip_pre_ampere, skip_pre_hopper @@ -15,31 +15,31 @@ @skip_pre_ampere def test_llm_int4_awq_quantization(): - config = ModelConfig(llama_model_path) - config.quant_config.quant_algo = QuantAlgo.W4A16_AWQ - assert config.quant_config.quant_mode.has_any_quant() + quant_config = QuantConfig() + quant_config.quant_algo = QuantAlgo.W4A16_AWQ + assert quant_config.quant_mode.has_any_quant() - llm = LLM(config) + llm = LLM(llama_model_path, quant_config=quant_config) sampling_params = SamplingParams(max_new_tokens=6) for output in llm.generate(["A B C"], sampling_params=sampling_params): print(output) - assert output.text == "D E F G H I" + assert output.outputs[0].text == "D E F G H I" @skip_pre_hopper def test_llm_fp8_quantization(): - config = ModelConfig(llama_model_path) - config.quant_config.quant_algo = QuantAlgo.FP8 - config.quant_config.kv_cache_quant_algo = QuantAlgo.FP8 + quant_config = QuantConfig() + quant_config.quant_algo = QuantAlgo.FP8 + quant_config.kv_cache_quant_algo = QuantAlgo.FP8 - assert config.quant_config.quant_mode.has_any_quant() + assert quant_config.quant_mode.has_any_quant() - llm = LLM(config) + llm = LLM(llama_model_path, quant_config=quant_config) sampling_params = SamplingParams(max_new_tokens=6) for output in llm.generate(["A B C"], sampling_params=sampling_params): print(output) - assert output.text == "D E F G H I" + assert output.outputs[0].text == "D E F G H I" if __name__ == "__main__": diff --git a/tests/hlapi/test_llm_utils.py b/tests/hlapi/test_llm_utils.py new file mode 100644 index 000000000..b85a919f8 --- /dev/null +++ b/tests/hlapi/test_llm_utils.py @@ -0,0 +1,212 @@ +import tempfile +from pathlib import Path + +import pytest +from transformers import AutoTokenizer + +from tensorrt_llm.builder import PluginConfig +from tensorrt_llm.hlapi.llm_utils import * + +try: + from test_llm import llama_model_path +except ImportError: + from .test_llm import llama_model_path + + +def test_ConfigArbitrator_basic(): + # the performance and functionality have conflict plugins config, keep the functionalies and disable the performance's + arb = _ConfigArbitrator() + arb.claim_perf("chunked_context", + config_name="plugin_config", + use_paged_context_fmha=True) + arb.claim_func("block_reuse", + config_name="plugin_config", + use_paged_context_fmha=False) + + plugin_config = PluginConfig() + arb(plugin_config=plugin_config) + + assert plugin_config.use_paged_context_fmha == False + + +def test_ConfigArbitrator_perf_conflict(): + # When performance-related plugins conflict, some performance-related feature maybe disabled to avoid conflict + # No exception should be raised in this case + arb = _ConfigArbitrator() + arb.claim_perf("perf0", + config_name="plugin_config", + use_paged_context_fmha=True) + arb.claim_perf("perf1", + config_name="plugin_config", + use_paged_context_fmha=False) + + plugin_config = PluginConfig() + arb(plugin_config=plugin_config) + + # The perf0 is claimed first, so the feature should be enabled + assert plugin_config.use_paged_context_fmha == True + + +def test_ConfigArbitrator_func_conflict(): + # When functional-related plugins conflict, an exception should be raised to remind the user to resolve the conflict + arb = _ConfigArbitrator() + arb.claim_func("func0", + config_name="plugin_config", + use_paged_context_fmha=True) + arb.claim_func("func1", + config_name="plugin_config", + use_paged_context_fmha=False) + + plugin_config = PluginConfig() + with pytest.raises(ConfigArbitrateError): + arb(plugin_config=plugin_config) + + +def test_ConfigArbitrator_setup(): + # Setup some pre-defined plugins configures + arb = _ConfigArbitrator() + arb.setup("pre-ampere is not supported", + config_name="plugin_config", + use_paged_context_fmha=False) + arb.claim_func("func0", + config_name="plugin_config", + use_paged_context_fmha=True) + + plugin_config = PluginConfig() + with pytest.raises(ConfigArbitrateError): + arb(plugin_config=plugin_config) + + +def test_ConfigArbitor_multi_configs(): + # A func claims two different configures, and the arbiter should be able to handle it + arb = _ConfigArbitrator() + + arb.claim_func("func0", + config_name="plugin_config", + use_paged_context_fmha=True) + arb.claim_func("func0", + config_name="kv_cache_config", + enable_block_reuse=False) + + plugin_config = PluginConfig() + kv_cache_config = KvCacheConfig() + + arb(plugin_config=plugin_config, kv_cache_config=kv_cache_config) + assert plugin_config.use_paged_context_fmha == True + assert kv_cache_config.enable_block_reuse == False + + +def test_ConfigArbitor_multi_configs_func_conflict(): + # A func claims two different configures with conflict options, the arbiter should raise an exception + arb = _ConfigArbitrator() + + arb.claim_func("func0", + config_name="plugin_config", + use_paged_context_fmha=True) + arb.claim_func("func0", + config_name="kv_cache_config", + enable_block_reuse=True) + + arb.claim_func("func1", + config_name="plugin_config", + use_paged_context_fmha=False) + + plugin_config = PluginConfig() + kv_cache_config = KvCacheConfig() + + with pytest.raises(ConfigArbitrateError): + arb(plugin_config=plugin_config, kv_cache_config=kv_cache_config) + + +def test_ConfigArbitor_multi_configs_perf_conflict(): + arb = _ConfigArbitrator() + + arb.claim_func("func0", + config_name="plugin_config", + use_paged_context_fmha=True) + arb.claim_func("func0", + config_name="kv_cache_config", + enable_block_reuse=True) + + arb.claim_perf("perf0", + config_name="plugin_config", + use_paged_context_fmha=False) # conflict with func0 + arb.claim_perf("perf0", config_name="plugin_config", + paged_kv_cache=False) # default value is True + + plugin_config = PluginConfig() + kv_cache_config = KvCacheConfig() + + arb(plugin_config=plugin_config, kv_cache_config=kv_cache_config) + + assert plugin_config.use_paged_context_fmha == True # perf0 is disabled + assert kv_cache_config.enable_block_reuse == True + assert plugin_config.paged_kv_cache == True # perf0 is disabled + + +def test_ConfigArbitor_perf_fallback(): + arb = _ConfigArbitrator() + + fallback_triggered = False + + def fallback(): + nonlocal fallback_triggered + fallback_triggered = True + + arb.claim_perf("perf0", + config_name="plugin_config", + use_paged_context_fmha=True) + arb.claim_perf("perf1", + config_name="plugin_config", + use_paged_context_fmha=False, + fallback=fallback) + + plugin_config = PluginConfig() + arb(plugin_config=plugin_config) + + assert plugin_config.use_paged_context_fmha == True + assert fallback_triggered == True + + +def test_ModelLoader(): + args = LlmArgs(llama_model_path) + args.setup() + + tokenizer = AutoTokenizer.from_pretrained(args.model) + + # Test with HF model + temp_dir = tempfile.TemporaryDirectory() + + def build_engine(): + model_loader = ModelLoader(args, tokenizer=tokenizer) + engine_dir = model_loader(engine_dir=Path(temp_dir.name)) + assert engine_dir + return engine_dir + + # Test with engine + args.model = build_engine() + args.setup() + assert args.model_format is _ModelFormatKind.TLLM_ENGINE + print(f'engine_dir: {args.model}') + model_loader = ModelLoader(args, tokenizer=tokenizer) + engine_dir = model_loader() + assert engine_dir == args.model + + +def test_CachedModelLoader(): + # CachedModelLoader enables engine caching and multi-gpu building + args = LlmArgs(llama_model_path) + args.enable_build_cache = True + args.setup() + stats = LlmBuildStats() + model_loader = CachedModelLoader(args, stats) + engine_dir = model_loader() + assert engine_dir + assert engine_dir.exists() and engine_dir.is_dir() + model_format = ModelLoader.get_model_format(engine_dir) + assert model_format is _ModelFormatKind.TLLM_ENGINE + + +if __name__ == '__main__': + #test_ModelLoader() + test_CachedModelLoader() diff --git a/tests/hlapi/test_mpi_session.py b/tests/hlapi/test_mpi_session.py index c0a36b24d..58d5eae10 100644 --- a/tests/hlapi/test_mpi_session.py +++ b/tests/hlapi/test_mpi_session.py @@ -1,6 +1,9 @@ import os import subprocess # nosec B404 +import pytest + +from tensorrt_llm.bindings.BuildInfo import ENABLE_MULTI_DEVICE from tensorrt_llm.hlapi.mpi_session import MPINodeState @@ -11,6 +14,7 @@ def task0(): return MPINodeState.state +@pytest.mark.skipif(not ENABLE_MULTI_DEVICE, reason="multi-device required") def test_mpi_session_basic(): from tensorrt_llm.hlapi.mpi_session import MpiPoolSession @@ -23,6 +27,7 @@ def test_mpi_session_basic(): assert results == [2, 2, 2, 2], results +@pytest.mark.skipif(not ENABLE_MULTI_DEVICE, reason="multi-device required") def test_mpi_session_multi_node(): nworkers = 4 test_case_file = os.path.join(os.path.dirname(__file__), "mpi_test_task.py") diff --git a/tests/model_api/test_model_api_multi_gpu.py b/tests/model_api/test_model_api_multi_gpu.py index b14e7c780..822c29e34 100644 --- a/tests/model_api/test_model_api_multi_gpu.py +++ b/tests/model_api/test_model_api_multi_gpu.py @@ -8,6 +8,7 @@ import torch from mpi4py import MPI from mpi4py.futures import MPIPoolExecutor +from transformers import AutoTokenizer import tensorrt_llm from tensorrt_llm import Mapping @@ -38,36 +39,39 @@ tensorrt_llm.logger.set_level('verbose') TP_SIZE = 2 +batch_input_text = [ + "Born in north-east France, Soyer trained as a", + "What is large language model?" +] +batch_output_text_expected_default = [ + "chef in Paris and London before moving to New York", + "\nLarge language model is a model that is" +] +batch_output_text_expected_mixtral = [ + "painter in Paris and then in Rome. He was", + "\n\nLarge language models (LLMs) are" +] + + +def get_batch_output_text_expected(model_name): + if model_name == "Mixtral-8x7B-v0.1": + return batch_output_text_expected_mixtral + else: + return batch_output_text_expected_default + # 76s on ipp1-1197, loading weights 18s (varies based on network speed), network/engine creation 27s @print_traceback_on_error def build_and_run_tp2(rank, model_name, engine_dir, use_auto_parallel): '''Do not save the engine, all in one LLaMAForCausalLM object ''' - input_text = [ - 'Born in north-east France, Soyer trained as a', - "What is large language model?" - ] - default_output = [ - "chef in Paris and London before moving to New York", - "\nLarge language model is a model that is" - ] - expected_outputs = { - "llama-models/llama-7b-hf": - default_output, - "Mixtral-8x7B-v0.1": [ - "painter in Paris and then in Rome. He was", - "\n\nLarge language models (LLMs) are" - ] - } - expected_output = expected_outputs.get(model_name, default_output) + batch_output_text_expected = get_batch_output_text_expected(model_name) tensorrt_llm.logger.set_level('verbose') torch.cuda.set_device(rank) max_batch_size, max_isl, max_osl = 8, 256, 256 - hf_model_dir = llm_models_root() / model_name - tokenizer_dir = hf_model_dir + hf_model_dir = str(llm_models_root() / model_name) mapping = Mapping(world_size=TP_SIZE, rank=rank, tp_size=TP_SIZE) auto_parallel_config = AutoParallelConfig() if use_auto_parallel: @@ -86,9 +90,7 @@ def build_and_run_tp2(rank, model_name, engine_dir, use_auto_parallel): ) # build and run by one llama object - llama = LLaMAForCausalLM.from_hugging_face(hf_model_dir, - 'float16', - mapping=mapping) + llama = LLaMAForCausalLM.from_hugging_face(hf_model_dir, mapping=mapping) engine = build( llama, BuildConfig(max_batch_size=max_batch_size, @@ -99,18 +101,22 @@ def build_and_run_tp2(rank, model_name, engine_dir, use_auto_parallel): engine.save(engine_dir) mpi_barrier() tensorrt_llm.logger.warning(f"Build finished for rank {rank}") - with ExecutorBindingsWorker(engine_dir, tokenizer_dir) as executor: + + tokenizer = AutoTokenizer.from_pretrained(hf_model_dir) + with ExecutorBindingsWorker(engine_dir) as executor: executor.block_subordinates() - for idx, output in enumerate( - executor.generate( - input_text, - sampling_params=SamplingParams(max_new_tokens=10))): - tensorrt_llm.logger.info(f"{rank} input: {input_text[idx]}") - tensorrt_llm.logger.info(f"{rank} output: {output.text}") - assert output.text.endswith( - expected_output[idx] - ), f"Expecting {expected_output[idx]}, got {output.text}" + batch_input_ids = [tokenizer.encode(inp) for inp in batch_input_text] + outputs = executor.generate( + batch_input_ids, sampling_params=SamplingParams(max_new_tokens=10)) + + for idx, output in enumerate(outputs): + tensorrt_llm.logger.info(f"{rank} input: {batch_input_text[idx]}") + output_text = tokenizer.decode(output.outputs[0].token_ids) + tensorrt_llm.logger.info(f"{rank} output: {output_text}") + assert output_text.endswith( + batch_output_text_expected[idx] + ), f"Expecting {batch_output_text_expected[idx]!r}, got {output_text!r}" mpi_barrier() return True @@ -126,13 +132,13 @@ def test_multi_gpu(model_name, use_auto_parallel): return if "Mixtral" in model_name and use_auto_parallel: pytest.skip("Auto parallel is not supported for Mixtral models") - engine_dir = tempfile.TemporaryDirectory() + engine_dir = tempfile.TemporaryDirectory().name with MPIPoolExecutor(max_workers=TP_SIZE) as executor: results = executor.map(build_and_run_tp2, (0, 1), [model_name] * 2, - [engine_dir.name] * 2, [use_auto_parallel] * 2) + [engine_dir] * 2, [use_auto_parallel] * 2) for r in results: - assert r == True + assert r is True if __name__ == "__main__": diff --git a/tests/model_api/test_model_level_api.py b/tests/model_api/test_model_level_api.py index 9fbf06772..d28f0f8b9 100644 --- a/tests/model_api/test_model_level_api.py +++ b/tests/model_api/test_model_level_api.py @@ -5,6 +5,7 @@ from contextlib import contextmanager from profile_utils import profile +from transformers import AutoTokenizer import tensorrt_llm from tensorrt_llm.builder import BuildConfig, build @@ -18,11 +19,11 @@ tensorrt_llm.logger.set_level('verbose') -input_text = [ - 'Born in north-east France, Soyer trained as a', +batch_input_text = [ + "Born in north-east France, Soyer trained as a", "What is large language model?" ] -expected_output = [ +batch_output_text_expected = [ "chef in Paris and London before moving to New York", "\nLarge language model is a model that is" ] @@ -50,32 +51,38 @@ def test_save_load(): This is optional, but users can store the engine into any folder they want, and use later ''' max_batch_size, max_isl, max_osl = 8, 256, 256 - hf_model_dir = llm_models_root() / "llama-models/llama-7b-hf" - tokenizer_dir = hf_model_dir + hf_model_dir = str(llm_models_root() / "llama-models/llama-7b-hf") with workspace("llama-save-load") as engine_dir: # build and run by one llama object - llama = LLaMAForCausalLM.from_hugging_face(hf_model_dir, 'float16') + llama = LLaMAForCausalLM.from_hugging_face(hf_model_dir) build_config = BuildConfig(max_batch_size=max_batch_size, max_input_len=max_isl, max_seq_len=max_osl + max_isl, plugin_config=llama.default_plugin_config()) - build_config.plugin_config.gemm_plugin = 'float16' # faster build + build_config.plugin_config.gemm_plugin = 'auto' # faster build engine = build(llama, build_config) engine.save(engine_dir) + tokenizer = AutoTokenizer.from_pretrained(hf_model_dir) + # use context manager to make sure the __exit__ can release the resources immediately - with GenerationExecutor.create(engine_dir, tokenizer_dir) as executor: - for idx, output in enumerate( - executor.generate( - input_text, - sampling_params=SamplingParams(max_new_tokens=10))): - tensorrt_llm.logger.info(f"Input: {input_text[idx]}") - tensorrt_llm.logger.info(f'Output: {output.text}') + with GenerationExecutor.create(engine_dir) as executor: + batch_input_ids = [ + tokenizer.encode(inp) for inp in batch_input_text + ] + outputs = executor.generate( + batch_input_ids, + sampling_params=SamplingParams(max_new_tokens=10)) + + for idx, output in enumerate(outputs): + tensorrt_llm.logger.info(f"Input: {batch_input_text[idx]}") + output_text = tokenizer.decode(output.outputs[0].token_ids) + tensorrt_llm.logger.info(f'Output: {output_text}') # note the output.text contains everything from the input, so only compare the suffix here. - assert output.text.endswith( - expected_output[idx] - ), f"Expecting and got:'{expected_output[idx]}' Got: '{output.text}'" + assert output_text.endswith( + batch_output_text_expected[idx] + ), f"Expecting and got: {batch_output_text_expected[idx]!r} Got: {output_text!r}" @profile(tag="fake-weights") @@ -83,35 +90,30 @@ def test_save_load(): def test_high_level_fake_weights(): '''sanity to make sure the flow works. ''' - input_text = [ - 'Born in north-east France, Soyer trained as a', - "What is large language model?" - ] max_batch_size, max_isl, max_osl = 8, 256, 256 - hf_model_dir = llm_models_root() / "llama-models/llama-7b-hf" + hf_model_dir = str(llm_models_root() / "llama-models/llama-7b-hf") # Fake weights, skipping save and load engine. Make it faster to sanity test - config = LLaMAConfig.from_hugging_face(hf_model_dir, dtype='float16') + config = LLaMAConfig.from_hugging_face(hf_model_dir) llama = LLaMAForCausalLM(config) build_config = BuildConfig(max_batch_size=max_batch_size, max_input_len=max_isl, max_seq_len=max_osl + max_isl, plugin_config=llama.default_plugin_config()) - build_config.plugin_config.gemm_plugin = 'float16' # faster build + build_config.plugin_config.gemm_plugin = 'auto' # faster build build(llama, build_config) @force_ampere def test_inflight_batching(): max_batch_size, max_isl, max_osl = 8, 256, 256 - hf_model_dir = llm_models_root() / "llama-models/llama-7b-hf" - tokenizer_dir = hf_model_dir + hf_model_dir = str(llm_models_root() / "llama-models/llama-7b-hf") - llama = LLaMAForCausalLM.from_hugging_face(hf_model_dir, 'float16') + llama = LLaMAForCausalLM.from_hugging_face(hf_model_dir) build_config = BuildConfig(max_batch_size=max_batch_size, max_input_len=max_isl, max_seq_len=max_osl + max_isl) - build_config.plugin_config.gemm_plugin = 'float16' # faster build + build_config.plugin_config.gemm_plugin = 'auto' # faster build engine = build(llama, build_config) engine_dir = "llama-ifb" @@ -119,32 +121,33 @@ def test_inflight_batching(): engine_dir = engine_temp.name engine.save(engine_dir) + tokenizer = AutoTokenizer.from_pretrained(hf_model_dir) + async def main(): - with GenerationExecutor.create(engine_dir, - tokenizer_dir) as async_engine: + with GenerationExecutor.create(engine_dir) as async_engine: async def generate_and_print(idx, inp): result = async_engine.generate_async( - inp, - streaming=False, - sampling_params=SamplingParams(max_new_tokens=10)) + tokenizer.encode(inp), + sampling_params=SamplingParams(max_new_tokens=10), + streaming=False) await result.aresult() - tensorrt_llm.logger.info(result.text) - assert result.text.endswith(expected_output[idx]) + output_text = tokenizer.decode(result.outputs[0].token_ids) + tensorrt_llm.logger.info(output_text) + assert output_text.endswith(batch_output_text_expected[idx]) - output = "" async for stream in async_engine.generate_async( - inp, - streaming=True, - sampling_params=SamplingParams(max_new_tokens=10)): - output += stream.text + ' ' + tokenizer.encode(inp), + sampling_params=SamplingParams(max_new_tokens=10), + streaming=True): + output_text = tokenizer.decode(stream.outputs[0].token_ids) tensorrt_llm.logger.info( - f"prompt: '{inp}', generation: '{output}'") + f"prompt: {inp!r}, generation: {output_text!r}") loop = asyncio.get_running_loop() tasks = [] # submit many request concurrently - for idx, inp in enumerate(input_text): + for idx, inp in enumerate(batch_input_text): task = loop.create_task(generate_and_print(idx, inp)) tasks.append(task) diff --git a/tests/model_api/test_model_quantization.py b/tests/model_api/test_model_quantization.py index bc823707e..454b609ee 100644 --- a/tests/model_api/test_model_quantization.py +++ b/tests/model_api/test_model_quantization.py @@ -1,7 +1,8 @@ import os import sys import tempfile -from pathlib import Path + +from transformers import AutoTokenizer import tensorrt_llm from tensorrt_llm.builder import BuildConfig, build @@ -16,17 +17,18 @@ tensorrt_llm.logger.set_level('info') +batch_input_text = [ + "Born in north-east France, Soyer trained as a", + "What is large language model?" +] + @force_ampere @skip_no_modelopt def test_int4_awq_quantization(): - input_text = [ - 'Born in north-east France, Soyer trained as a', - "What is large language model?" - ] + max_batch_size, max_isl, max_osl = 8, 256, 256 hf_model_dir = llm_models_root() / "llama-models/llama-7b-hf" - tokenizer_dir = hf_model_dir checkpoint_dir = tempfile.TemporaryDirectory("llama-checkpoint").name quant_config = QuantConfig(QuantAlgo.W4A16_AWQ) LLaMAForCausalLM.quantize(hf_model_dir, @@ -48,26 +50,24 @@ def test_int4_awq_quantization(): engine_temp = tempfile.TemporaryDirectory(engine_dir) engine_dir = engine_temp.name engine.save(engine_dir) - with GenerationExecutor.create(Path(engine_dir), tokenizer_dir) as executor: - for idx, output in enumerate( - executor.generate( - input_text, - sampling_params=SamplingParams(max_new_tokens=10))): - print(f"Input: {input_text[idx]}") - print(f'Output: {output.text}') + + tokenizer = AutoTokenizer.from_pretrained(hf_model_dir) + with GenerationExecutor.create(engine_dir) as executor: + batch_input_ids = [tokenizer.encode(inp) for inp in batch_input_text] + outputs = executor.generate( + batch_input_ids, sampling_params=SamplingParams(max_new_tokens=10)) + for idx, output in enumerate(outputs): + print(f"Input: {batch_input_text[idx]}") + output_text = tokenizer.decode(output.outputs[0].token_ids) + print(f'Output: {output_text}') # TODO: TRTLLM-185, check the score when the test infra is ready, hard coded value is not stable, cause flaky tests in L0 @skip_pre_ada @skip_no_modelopt def test_fp8_quantization(): - input_text = [ - 'Born in north-east France, Soyer trained as a', - "What is large language model?" - ] max_batch_size, max_isl, max_osl = 8, 256, 256 - hf_model_dir = llm_models_root() / "llama-models/llama-7b-hf" - tokenizer_dir = hf_model_dir + hf_model_dir = str(llm_models_root() / "llama-models/llama-7b-hf") checkpoint_dir = tempfile.TemporaryDirectory("llama-checkpoint").name quant_config = QuantConfig(QuantAlgo.FP8) @@ -88,13 +88,17 @@ def test_fp8_quantization(): engine_temp = tempfile.TemporaryDirectory(engine_dir) engine_dir = engine_temp.name engine.save(engine_dir) - with GenerationExecutor.create(Path(engine_dir), tokenizer_dir) as executor: - for idx, output in enumerate( - executor.generate( - input_text, - sampling_params=SamplingParams(max_new_tokens=10))): - print(f"Input: {input_text[idx]}") - print(f'Output: {output.text}') + + tokenizer = AutoTokenizer.from_pretrained(hf_model_dir) + with GenerationExecutor.create(engine_dir) as executor: + batch_input_ids = [tokenizer.encode(inp) for inp in batch_input_text] + outputs = executor.generate( + batch_input_ids, sampling_params=SamplingParams(max_new_tokens=10)) + + for idx, output in enumerate(outputs): + print(f"Input: {batch_input_text[idx]}") + output_text = tokenizer.decode(output.outputs[0].token_ids) + print(f'Output: {output_text}') # TODO: TRTLLM-185, check the score when the test infra is ready, hard coded value is not stable, cause flaky tests in L0 diff --git a/windows/setup_build_env.ps1 b/windows/setup_build_env.ps1 index 4983ae7e8..7e01414f9 100644 --- a/windows/setup_build_env.ps1 +++ b/windows/setup_build_env.ps1 @@ -48,18 +48,18 @@ if (-not $skipVSBuildTools) { # Install TensorRT 10.0.1 for TensorRT-LLM if (-not $skipTRT) { Write-Output "Downloading TensorRT" - Invoke-WebRequest -Uri 'https://developer.nvidia.com/downloads/compute/machine-learning/tensorrt/10.0.1/zip/TensorRT-10.0.1.6.Windows10.win10.cuda-12.4.zip' -OutFile 'TensorRT-10.0.1.6.zip' + Invoke-WebRequest -Uri 'https://developer.nvidia.com/downloads/compute/machine-learning/tensorrt/10.1.0/zip/TensorRT-10.1.0.27.Windows.win10.cuda-12.4.zip' -OutFile 'TensorRT-10.1.0.27.zip' Write-Output "Extracting TensorRT" # Get path $absolutePath = Resolve-Path $TRTPath - Expand-Archive -Path '.\TensorRT-10.0.1.6.zip' -DestinationPath $absolutePath + Expand-Archive -Path '.\TensorRT-10.1.0.27.zip' -DestinationPath $absolutePath Write-Output "Removing TensorRT zip" - Remove-Item -Path 'TensorRT-10.0.1.6.zip' -Force + Remove-Item -Path 'TensorRT-10.1.0.27.zip' -Force Write-Output "Adding TensorRT to system Path" - [Environment]::SetEnvironmentVariable('Path', "$env:Path;$absolutePath\TensorRT-10.0.1.6\lib", [EnvironmentVariableTarget]::Machine) + [Environment]::SetEnvironmentVariable('Path', "$env:Path;$absolutePath\TensorRT-10.1.0.27\lib", [EnvironmentVariableTarget]::Machine) Write-Output "Installing TensorRT Python wheel" - python3 -m pip install $absolutePath\TensorRT-10.0.1.6\python\tensorrt-10.0.1-cp310-none-win_amd64.whl - Write-Output "Done TensorRT installation at '$absolutePath\TensorRT-10.0.1.6'" + python3 -m pip install $absolutePath\TensorRT-10.1.0.27\python\tensorrt-10.1.0-cp310-none-win_amd64.whl + Write-Output "Done TensorRT installation at '$absolutePath\TensorRT-10.1.0.27'" } else { Write-Output "Skipping TensorRT installation" } diff --git a/windows/setup_env.ps1 b/windows/setup_env.ps1 index d5a8a13a7..94926d367 100644 --- a/windows/setup_env.ps1 +++ b/windows/setup_env.ps1 @@ -4,10 +4,13 @@ param ( [switch]$skipPython, [switch]$skipMPI = $true, [switch]$skipCUDNN = $true, - [string]$cudaVersion, #CUDA version defaults to 12.4, specify otherwise + [string]$cudaVersion, #CUDA version defaults to $defaultCudaVersion, specify otherwise [switch]$skipTRT = $true ) +# Default CUDA version if not specified by user. +$defaultCudaVersion = "12.4.1" + # Set the error action preference to 'Stop' for the entire script. # Respond to non-terminating errors by stopping execution and displaying an error message. $ErrorActionPreference = 'Stop' @@ -20,35 +23,35 @@ $ErrorActionPreference = 'Stop' New-Item -Path "$($env:LOCALAPPDATA)\trt_env_outlog.txt" -Force -# Install CUDA, default to 12.4 +# Install CUDA if (-not $skipCUDA){ - if($cudaVersion){ - $cudaVer = "NVIDIA CUDA Toolkit " + $cudaVersion - } else { - $cudaVersion = 12.4 - $cudaVer = "NVIDIA CUDA Toolkit 12.4" + if(-not ($cudaVersion)){ + $cudaVersion = $defaultCudaVersion } + $cudaVer = "NVIDIA CUDA Toolkit " + $cudaVersion if (-not (Get-Package -Name $cudaVer -EA Ignore)) { - Write-Output "Downloading CUDA - this will take a while" + Write-Output "Downloading $cudaVer - this will take a while" $ProgressPreference = 'SilentlyContinue' - if ($cudaVersion -eq 12.2){ - Invoke-WebRequest -Uri 'https://developer.download.nvidia.com/compute/cuda/12.2.2/local_installers/cuda_12.2.2_537.13_windows.exe' -OutFile 'cuda_installer.exe' - } elseif ($cudaVersion -eq 12.3){ - Invoke-WebRequest -Uri 'https://developer.download.nvidia.com/compute/cuda/12.3.2/local_installers/cuda_12.3.2_546.12_windows.exe' -OutFile 'cuda_installer.exe' - } elseif ($cudaVersion -eq 12.4){ - Invoke-WebRequest -Uri 'https://developer.download.nvidia.com/compute/cuda/12.4.0/local_installers/cuda_12.4.0_551.61_windows.exe' -OutFile 'cuda_installer.exe' + if ($cudaVersion -eq "12.2"){ + $cudaUri = 'https://developer.download.nvidia.com/compute/cuda/12.2.2/local_installers/cuda_12.2.2_537.13_windows.exe' + } elseif ($cudaVersion -eq "12.3"){ + $cudaUri = 'https://developer.download.nvidia.com/compute/cuda/12.3.2/local_installers/cuda_12.3.2_546.12_windows.exe' + } elseif ($cudaVersion -eq "12.4"){ + $cudaUri = 'https://developer.download.nvidia.com/compute/cuda/12.4.0/local_installers/cuda_12.4.0_551.61_windows.exe' + } elseif ($cudaVersion -eq "12.4.1"){ + $cudaUri = 'https://developer.download.nvidia.com/compute/cuda/12.4.1/local_installers/cuda_12.4.1_551.78_windows.exe' } else { $cudaUri = Read-Host "Please go to https://developer.nvidia.com/cuda-downloads and input the url of the CUDA version you wish to use" - Invoke-WebRequest -Uri $cudaUri -OutFile 'cuda_installer.exe' } + Invoke-WebRequest -Uri $cudaUri -OutFile 'cuda_installer.exe' - Write-Output "Installing CUDA silently - this will take a while" + Write-Output "Installing $cudaVer silently - this will take a while" Start-Process -Wait -FilePath 'cuda_installer.exe' -ArgumentList '-s' $ProgressPreference = 'Continue' Write-Output "Removing CUDA installer" Remove-Item -Path 'cuda_installer.exe' -Force - Write-Output "Done CUDA installation at 'C:\Program Files\NVIDIA Corporation' and 'C:\Program Files\NVIDIA GPU Computing Toolkit'" + Write-Output "Done $cudaVer installation at 'C:\Program Files\NVIDIA Corporation' and 'C:\Program Files\NVIDIA GPU Computing Toolkit'" Add-Content -Path $env:LOCALAPPDATA\trt_env_outlog.txt -Value "0" Add-Content -Path $env:LOCALAPPDATA\trt_env_outlog.txt -Value $cudaVer } else { @@ -141,7 +144,7 @@ if(-not $skipCUDNN){ Add-Content -Path $env:LOCALAPPDATA\trt_env_outlog.txt -Value "0" New-Item -Path $env:LOCALAPPDATA\CUDNN -ItemType Directory -Force $ProgressPreference = 'SilentlyContinue' - Invoke-WebRequest -Uri 'https://developer.download.nvidia.com/compute/cudnn/redist/cudnn/windows-x86_64/cudnn-windows-x86_64-8.9.7.29_cuda12-archive.zip' -OutFile $env:LOCALAPPDATA\CUDNN\cudnn.zip + Invoke-WebRequest -Uri 'https://developer.download.nvidia.com/compute/cudnn/redist/cudnn/windows-x86_64/cudnn-windows-x86_64-9.1.0.70_cuda12-archive.zip' -OutFile $env:LOCALAPPDATA\CUDNN\cudnn.zip Expand-Archive -Path $env:LOCALAPPDATA\CUDNN\cudnn.zip -DestinationPath $env:LOCALAPPDATA\CUDNN\cudnn_unzip New-Item -Path ".\" -Name "CUDNN" -ItemType "directory" @@ -151,9 +154,9 @@ if(-not $skipCUDNN){ New-Item -Path $binPath -ItemType Directory New-Item -Path $includePath -ItemType Directory New-Item -Path $libPath -ItemType Directory - Copy-Item -Path "$env:LOCALAPPDATA\CUDNN\cudnn_unzip\cudnn-windows-x86_64-8.9.7.29_cuda12-archive\bin\*" -Destination $binPath - Copy-Item -Path "$env:LOCALAPPDATA\CUDNN\cudnn_unzip\cudnn-windows-x86_64-8.9.7.29_cuda12-archive\include\*" -Destination $includePath - Copy-Item -Path "$env:LOCALAPPDATA\CUDNN\cudnn_unzip\cudnn-windows-x86_64-8.9.7.29_cuda12-archive\lib\x64\*" -Destination $libPath + Copy-Item -Path "$env:LOCALAPPDATA\CUDNN\cudnn_unzip\cudnn-windows-x86_64-9.1.0.70_cuda12-archive\bin\*" -Destination $binPath + Copy-Item -Path "$env:LOCALAPPDATA\CUDNN\cudnn_unzip\cudnn-windows-x86_64-9.1.0.70_cuda12-archive\include\*" -Destination $includePath + Copy-Item -Path "$env:LOCALAPPDATA\CUDNN\cudnn_unzip\cudnn-windows-x86_64-9.1.0.70_cuda12-archive\lib\x64\*" -Destination $libPath [Environment]::SetEnvironmentVariable("CUDNN", "$PWD;$binPath;$includePath;$libPath", [EnvironmentVariableTarget]::Machine) @@ -176,10 +179,10 @@ if (-not ($skipTRT)) { Write-Output "Grabbing TensorRT..." $ProgressPreference = 'SilentlyContinue' New-Item -Path .\TensorRT -ItemType Directory - Invoke-WebRequest -Uri 'https://developer.nvidia.com/downloads/compute/machine-learning/tensorrt/10.0.1/zip/TensorRT-10.0.1.6.Windows10.win10.cuda-12.4.zip' -OutFile .\TensorRT\trt.zip + Invoke-WebRequest -Uri 'https://developer.nvidia.com/downloads/compute/machine-learning/tensorrt/10.1.0/zip/TensorRT-10.1.0.27.Windows.win10.cuda-12.4.zip' -OutFile .\TensorRT\trt.zip Expand-Archive -Path .\TensorRT\trt.zip -DestinationPath .\TensorRT\ Remove-Item -Path .\TensorRT\trt.zip -Force - $trtPath = Join-Path $TRT_BASE TensorRT-10.0.1.6 + $trtPath = Join-Path $TRT_BASE TensorRT-10.1.0.27 Write-Output "TensorRT installed at ${trtPath}" $trtSubPaths = @{