| 1 | // SPDX-License-Identifier: Apache-2.0 |
| 2 | // SPDX-FileCopyrightText: Copyright contributors to the vLLM project |
| 3 | // |
| 4 | // Fused vocab-parallel embedding lookup. |
| 5 | // |
| 6 | // With TP > 1 each rank owns a slice of the vocabulary, so the eager path is a |
| 7 | // range mask, an id shift, an int64 cast, a gather and a masked_fill_ of the |
| 8 | // gathered rows. This kernel does all of it in one pass: rows owned by another |
| 9 | // rank are written as zeros, which keeps the following all-reduce exact. |
| 10 | // |
| 11 | // The gather is a pure row copy, so the kernel is templated on a byte-vector |
| 12 | // type instead of the value type and works for any embedding dtype. |
| 13 | |
| 14 | #include "torch_utils.h" |
| 15 | |
| 16 | #include "ops.h" |
| 17 | |
| 18 | #include <algorithm> |
| 19 | #include <cstdint> |
| 20 | |
| 21 | namespace vllm::vocab_embedding { |
| 22 | |
| 23 | constexpr int kBlockThreads = 256; |
| 24 | // Enough token-blocks to fill the device several times over; the token loop |
| 25 | // strides over the rest. |
| 26 | constexpr int kTokenWaves = 4; |
| 27 | |
| 28 | template <typename idx_t, typename vec_t> |
| 29 | __global__ void vocab_parallel_embedding_kernel( |
| 30 | vec_t* __restrict__ out, // [num_tokens, vecs_per_row] |
| 31 | const idx_t* __restrict__ input_ids, // [num_tokens] |
| 32 | const vec_t* __restrict__ weight, // [num_rows, vecs_per_row] |
| 33 | const int64_t num_tokens, const int64_t vecs_per_row, |
| 34 | const int64_t org_vocab_start_index, const int64_t org_vocab_end_index, |
| 35 | const int64_t added_vocab_start_index, const int64_t added_vocab_end_index, |
| 36 | const int64_t added_row_offset) { |
| 37 | const int64_t vec_begin = |
| 38 | static_cast<int64_t>(blockIdx.x) * blockDim.x + threadIdx.x; |
| 39 | const int64_t vec_stride = static_cast<int64_t>(gridDim.x) * blockDim.x; |
| 40 | |
| 41 | for (int64_t token = blockIdx.y; token < num_tokens; token += gridDim.y) { |
| 42 | const int64_t id = static_cast<int64_t>(input_ids[token]); |
| 43 | const bool in_org = id >= org_vocab_start_index && id < org_vocab_end_index; |
| 44 | const bool in_added = |
| 45 | id >= added_vocab_start_index && id < added_vocab_end_index; |
| 46 | vec_t* out_row = out + token * vecs_per_row; |
| 47 | |
| 48 | if (in_org || in_added) { |
| 49 | const int64_t row = |
| 50 | in_org ? id - org_vocab_start_index : id + added_row_offset; |
| 51 | const vec_t* weight_row = weight + row * vecs_per_row; |
| 52 | for (int64_t v = vec_begin; v < vecs_per_row; v += vec_stride) { |
| 53 | out_row[v] = weight_row[v]; |
| 54 | } |
| 55 | } else { |
| 56 | // Token belongs to another rank's shard: contribute zeros. |
| 57 | const vec_t zero = vec_t(); |
| 58 | for (int64_t v = vec_begin; v < vecs_per_row; v += vec_stride) { |
| 59 | out_row[v] = zero; |
| 60 | } |
| 61 | } |
| 62 | } |
| 63 | } |
| 64 | |
| 65 | template <typename idx_t, typename vec_t> |
| 66 | void launch(torch::stable::Tensor& out, const torch::stable::Tensor& input_ids, |
| 67 | const torch::stable::Tensor& weight, int64_t org_vocab_start_index, |
| 68 | int64_t org_vocab_end_index, int64_t added_vocab_start_index, |
| 69 | int64_t added_vocab_end_index, int64_t added_row_offset, |
| 70 | int64_t num_tokens, int64_t row_bytes, cudaStream_t stream) { |
| 71 | const int64_t vecs_per_row = row_bytes / static_cast<int64_t>(sizeof(vec_t)); |
| 72 | const int grid_x = static_cast<int>( |
| 73 | std::min<int64_t>((vecs_per_row + kBlockThreads - 1) / kBlockThreads, |
| 74 | static_cast<int64_t>(kBlockThreads))); |
| 75 | const int grid_y = static_cast<int>(std::min<int64_t>( |
| 76 | num_tokens, static_cast<int64_t>(kTokenWaves) * |
| 77 | get_device_prop()->multiProcessorCount)); |
| 78 | |
| 79 | vocab_parallel_embedding_kernel<idx_t, vec_t> |
| 80 | <<<dim3(grid_x, grid_y), kBlockThreads, 0, stream>>>( |
| 81 | reinterpret_cast<vec_t*>(out.mutable_data_ptr()), |
| 82 | reinterpret_cast<const idx_t*>(input_ids.const_data_ptr()), |
| 83 | reinterpret_cast<const vec_t*>(weight.const_data_ptr()), num_tokens, |
| 84 | vecs_per_row, org_vocab_start_index, org_vocab_end_index, |
| 85 | added_vocab_start_index, added_vocab_end_index, added_row_offset); |
| 86 | } |
| 87 | |
| 88 | template <typename idx_t> |
| 89 | void dispatch_vec_width( |
| 90 | torch::stable::Tensor& out, const torch::stable::Tensor& input_ids, |
| 91 | const torch::stable::Tensor& weight, int64_t org_vocab_start_index, |
| 92 | int64_t org_vocab_end_index, int64_t added_vocab_start_index, |
| 93 | int64_t added_vocab_end_index, int64_t added_row_offset, int64_t num_tokens, |
| 94 | int64_t row_bytes, cudaStream_t stream) { |
| 95 | const uintptr_t addrs = reinterpret_cast<uintptr_t>(out.mutable_data_ptr()) | |
| 96 | reinterpret_cast<uintptr_t>(weight.const_data_ptr()) | |
| 97 | static_cast<uintptr_t>(row_bytes); |
| 98 | |
| 99 | #define VLLM_LAUNCH_VOCAB_EMBEDDING(vec_t) \ |
| 100 | launch<idx_t, vec_t>(out, input_ids, weight, org_vocab_start_index, \ |
| 101 | org_vocab_end_index, added_vocab_start_index, \ |
| 102 | added_vocab_end_index, added_row_offset, num_tokens, \ |
| 103 | row_bytes, stream) |
| 104 | |
| 105 | if (addrs % 16 == 0) { |
| 106 | VLLM_LAUNCH_VOCAB_EMBEDDING(uint4); |
| 107 | } else if (addrs % 8 == 0) { |
| 108 | VLLM_LAUNCH_VOCAB_EMBEDDING(uint2); |
| 109 | } else if (addrs % 4 == 0) { |
| 110 | VLLM_LAUNCH_VOCAB_EMBEDDING(uint32_t); |
| 111 | } else if (addrs % 2 == 0) { |
| 112 | VLLM_LAUNCH_VOCAB_EMBEDDING(uint16_t); |
| 113 | } else { |
| 114 | VLLM_LAUNCH_VOCAB_EMBEDDING(uint8_t); |
| 115 | } |
| 116 | #undef VLLM_LAUNCH_VOCAB_EMBEDDING |
| 117 | } |
| 118 | |
| 119 | } // namespace vllm::vocab_embedding |
| 120 | |
| 121 | void vocab_parallel_embedding( |
| 122 | torch::stable::Tensor& out, const torch::stable::Tensor& input_ids, |
| 123 | const torch::stable::Tensor& weight, int64_t org_vocab_start_index, |
| 124 | int64_t org_vocab_end_index, int64_t num_org_vocab_padding, |
| 125 | int64_t added_vocab_start_index, int64_t added_vocab_end_index) { |
| 126 | STD_TORCH_CHECK(weight.dim() == 2, "weight must be 2D"); |
| 127 | STD_TORCH_CHECK(out.dim() == 2, "out must be 2D"); |
| 128 | STD_TORCH_CHECK(out.size(1) == weight.size(1), |
| 129 | "out and weight must share the embedding dim"); |
| 130 | STD_TORCH_CHECK(out.size(0) == input_ids.numel(), |
| 131 | "out must have one row per input id"); |
| 132 | STD_TORCH_CHECK(out.scalar_type() == weight.scalar_type(), |
| 133 | "out and weight must have the same dtype"); |
| 134 | STD_TORCH_CHECK(input_ids.is_contiguous() && weight.is_contiguous() && |
| 135 | out.is_contiguous(), |
| 136 | "vocab_parallel_embedding requires contiguous tensors"); |
| 137 | |
| 138 | const int64_t num_tokens = input_ids.numel(); |
| 139 | if (num_tokens == 0) return; |
| 140 | |
| 141 | const int64_t row_bytes = weight.size(1) * weight.element_size(); |
| 142 | // Offset that maps an added-vocab (LoRA) id onto its local row, mirroring |
| 143 | // get_masked_input_and_mask(). |
| 144 | const int64_t added_row_offset = org_vocab_end_index - org_vocab_start_index + |
| 145 | num_org_vocab_padding - |
| 146 | added_vocab_start_index; |
| 147 | |
| 148 | const torch::stable::accelerator::DeviceGuard device_guard( |
| 149 | out.get_device_index()); |
| 150 | const cudaStream_t stream = get_current_cuda_stream(); |
| 151 | |
| 152 | if (input_ids.scalar_type() == torch::headeronly::ScalarType::Long) { |
| 153 | vllm::vocab_embedding::dispatch_vec_width<int64_t>( |
| 154 | out, input_ids, weight, org_vocab_start_index, org_vocab_end_index, |
| 155 | added_vocab_start_index, added_vocab_end_index, added_row_offset, |
| 156 | num_tokens, row_bytes, stream); |
| 157 | } else { |
| 158 | STD_TORCH_CHECK( |
| 159 | input_ids.scalar_type() == torch::headeronly::ScalarType::Int, |
| 160 | "input_ids must be int32 or int64"); |
| 161 | vllm::vocab_embedding::dispatch_vec_width<int32_t>( |
| 162 | out, input_ids, weight, org_vocab_start_index, org_vocab_end_index, |
| 163 | added_vocab_start_index, added_vocab_end_index, added_row_offset, |
| 164 | num_tokens, row_bytes, stream); |
| 165 | } |
| 166 | } |