forked from Karylab-cklius/vllm
Compare commits
7
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
fadfefcc6e | ||
|
|
09e4576f65 | ||
|
|
3ed7b1e6e0 | ||
|
|
e8f9dbc369 | ||
|
|
de35c06c66 | ||
|
|
c0745a851a | ||
|
|
b5ca9c3557 |
+2
-1
@@ -262,7 +262,8 @@ void get_cutlass_moe_mm_data(
|
||||
torch::Tensor& problem_sizes1, torch::Tensor& problem_sizes2,
|
||||
torch::Tensor& input_permutation, torch::Tensor& output_permutation,
|
||||
const int64_t num_experts, const int64_t n, const int64_t k,
|
||||
const std::optional<torch::Tensor>& blockscale_offsets);
|
||||
const std::optional<torch::Tensor>& blockscale_offsets,
|
||||
const bool is_gated);
|
||||
|
||||
void get_cutlass_moe_mm_problem_sizes_from_expert_offsets(
|
||||
const torch::Tensor& expert_first_token_offset,
|
||||
|
||||
@@ -17,8 +17,11 @@ __global__ void compute_problem_sizes(const int32_t* __restrict__ topk_ids,
|
||||
int32_t* problem_sizes2,
|
||||
int32_t* atomic_buffer,
|
||||
const int topk_length, const int n,
|
||||
const int k) {
|
||||
const int k, const bool is_gated) {
|
||||
int expert_id = blockIdx.x;
|
||||
// For gated activations (gate + up), first GEMM output is 2*n.
|
||||
// For non-gated activations (up only), first GEMM output is n.
|
||||
int const n1 = is_gated ? 2 * n : n;
|
||||
|
||||
int occurrences = 0;
|
||||
for (int i = threadIdx.x; i < topk_length; i += THREADS_PER_EXPERT) {
|
||||
@@ -31,13 +34,13 @@ __global__ void compute_problem_sizes(const int32_t* __restrict__ topk_ids,
|
||||
int final_occurrences = atomic_buffer[expert_id];
|
||||
if constexpr (!SWAP_AB) {
|
||||
problem_sizes1[expert_id * 3] = final_occurrences;
|
||||
problem_sizes1[expert_id * 3 + 1] = 2 * n;
|
||||
problem_sizes1[expert_id * 3 + 1] = n1;
|
||||
problem_sizes1[expert_id * 3 + 2] = k;
|
||||
problem_sizes2[expert_id * 3] = final_occurrences;
|
||||
problem_sizes2[expert_id * 3 + 1] = k;
|
||||
problem_sizes2[expert_id * 3 + 2] = n;
|
||||
} else {
|
||||
problem_sizes1[expert_id * 3] = 2 * n;
|
||||
problem_sizes1[expert_id * 3] = n1;
|
||||
problem_sizes1[expert_id * 3 + 1] = final_occurrences;
|
||||
problem_sizes1[expert_id * 3 + 2] = k;
|
||||
problem_sizes2[expert_id * 3] = k;
|
||||
@@ -107,13 +110,11 @@ __global__ void compute_arg_sorts(const int32_t* __restrict__ topk_ids,
|
||||
}
|
||||
|
||||
namespace {
|
||||
inline void launch_compute_problem_sizes(const torch::Tensor& topk_ids,
|
||||
torch::Tensor& problem_sizes1,
|
||||
torch::Tensor& problem_sizes2,
|
||||
torch::Tensor& atomic_buffer,
|
||||
int64_t num_experts, int64_t n,
|
||||
int64_t k, cudaStream_t stream,
|
||||
const bool swap_ab) {
|
||||
inline void launch_compute_problem_sizes(
|
||||
const torch::Tensor& topk_ids, torch::Tensor& problem_sizes1,
|
||||
torch::Tensor& problem_sizes2, torch::Tensor& atomic_buffer,
|
||||
int64_t num_experts, int64_t n, int64_t k, cudaStream_t stream,
|
||||
const bool swap_ab, const bool is_gated) {
|
||||
int num_threads = min(THREADS_PER_EXPERT, topk_ids.numel());
|
||||
|
||||
auto const* topk_ptr = topk_ids.data_ptr<int32_t>();
|
||||
@@ -125,7 +126,7 @@ inline void launch_compute_problem_sizes(const torch::Tensor& topk_ids,
|
||||
compute_problem_sizes<SwapAB><<<num_experts, num_threads, 0, stream>>>(
|
||||
topk_ptr, ps1_ptr, ps2_ptr, atomic_ptr,
|
||||
static_cast<int>(topk_ids.numel()), static_cast<int>(n),
|
||||
static_cast<int>(k));
|
||||
static_cast<int>(k), is_gated);
|
||||
});
|
||||
}
|
||||
} // namespace
|
||||
@@ -222,7 +223,8 @@ void get_cutlass_moe_mm_data_caller(
|
||||
torch::Tensor& problem_sizes1, torch::Tensor& problem_sizes2,
|
||||
torch::Tensor& input_permutation, torch::Tensor& output_permutation,
|
||||
const int64_t num_experts, const int64_t n, const int64_t k,
|
||||
const std::optional<torch::Tensor>& blockscale_offsets) {
|
||||
const std::optional<torch::Tensor>& blockscale_offsets,
|
||||
const bool is_gated) {
|
||||
auto stream = at::cuda::getCurrentCUDAStream(topk_ids.device().index());
|
||||
auto options_int32 =
|
||||
torch::TensorOptions().dtype(torch::kInt32).device(topk_ids.device());
|
||||
@@ -236,7 +238,7 @@ void get_cutlass_moe_mm_data_caller(
|
||||
|
||||
launch_compute_problem_sizes(topk_ids, problem_sizes1, problem_sizes2,
|
||||
atomic_buffer, num_experts, n, k, stream,
|
||||
may_swap_ab);
|
||||
may_swap_ab, is_gated);
|
||||
|
||||
if (blockscale_offsets.has_value()) {
|
||||
// fp4 path
|
||||
|
||||
@@ -75,7 +75,8 @@ void get_cutlass_moe_mm_data_caller(
|
||||
torch::Tensor& problem_sizes1, torch::Tensor& problem_sizes2,
|
||||
torch::Tensor& input_permutation, torch::Tensor& output_permutation,
|
||||
const int64_t num_experts, const int64_t n, const int64_t k,
|
||||
const std::optional<torch::Tensor>& blockscale_offsets);
|
||||
const std::optional<torch::Tensor>& blockscale_offsets,
|
||||
const bool is_gated);
|
||||
|
||||
void get_cutlass_moe_mm_problem_sizes_from_expert_offsets_caller(
|
||||
const torch::Tensor& expert_first_token_offset,
|
||||
@@ -278,7 +279,8 @@ void get_cutlass_moe_mm_data(
|
||||
torch::Tensor& problem_sizes1, torch::Tensor& problem_sizes2,
|
||||
torch::Tensor& input_permutation, torch::Tensor& output_permutation,
|
||||
const int64_t num_experts, const int64_t n, const int64_t k,
|
||||
const std::optional<torch::Tensor>& blockscale_offsets) {
|
||||
const std::optional<torch::Tensor>& blockscale_offsets,
|
||||
const bool is_gated) {
|
||||
// This function currently gets compiled only if we have a valid cutlass moe
|
||||
// mm to run it for.
|
||||
int32_t version_num = get_sm_version_num();
|
||||
@@ -288,7 +290,7 @@ void get_cutlass_moe_mm_data(
|
||||
get_cutlass_moe_mm_data_caller(topk_ids, expert_offsets, problem_sizes1,
|
||||
problem_sizes2, input_permutation,
|
||||
output_permutation, num_experts, n, k,
|
||||
blockscale_offsets);
|
||||
blockscale_offsets, is_gated);
|
||||
return;
|
||||
#endif
|
||||
TORCH_CHECK_NOT_IMPLEMENTED(
|
||||
|
||||
@@ -489,8 +489,8 @@ TORCH_LIBRARY_EXPAND(TORCH_EXTENSION_NAME, ops) {
|
||||
" Tensor! problem_sizes1, Tensor! problem_sizes2, "
|
||||
" Tensor! input_permutation, "
|
||||
" Tensor! output_permutation, int num_experts, "
|
||||
" int n, int k, Tensor? blockscale_offsets) -> "
|
||||
"()");
|
||||
" int n, int k, Tensor? blockscale_offsets, "
|
||||
" bool is_gated) -> ()");
|
||||
ops.impl("get_cutlass_moe_mm_data", torch::kCUDA, &get_cutlass_moe_mm_data);
|
||||
|
||||
// compute per-expert problem sizes from expert_first_token_offset
|
||||
|
||||
@@ -625,6 +625,46 @@ curl -s http://localhost:8000/rerank -H "Content-Type: application/json" -d '{
|
||||
}'
|
||||
```
|
||||
|
||||
### ColQwen3.5 Multi-Modal Late Interaction Models
|
||||
|
||||
ColQwen3.5 is based on [ColPali](https://arxiv.org/abs/2407.01449), extending ColBERT's late interaction approach to **multi-modal** inputs. It uses the Qwen3.5 hybrid backbone (linear + full attention) and produces per-token L2-normalized vectors for MaxSim scoring.
|
||||
|
||||
| Architecture | Backbone | Example HF Models |
|
||||
| - | - | - |
|
||||
| `ColQwen3_5` | Qwen3.5 | `athrael-soju/colqwen3.5-4.5B` |
|
||||
|
||||
Start the server:
|
||||
|
||||
```shell
|
||||
vllm serve athrael-soju/colqwen3.5-4.5B --max-model-len 4096
|
||||
```
|
||||
|
||||
Then you can use the rerank endpoint:
|
||||
|
||||
```shell
|
||||
curl -s http://localhost:8000/rerank -H "Content-Type: application/json" -d '{
|
||||
"model": "athrael-soju/colqwen3.5-4.5B",
|
||||
"query": "What is machine learning?",
|
||||
"documents": [
|
||||
"Machine learning is a subset of artificial intelligence.",
|
||||
"Python is a programming language.",
|
||||
"Deep learning uses neural networks."
|
||||
]
|
||||
}'
|
||||
```
|
||||
|
||||
Or the score endpoint:
|
||||
|
||||
```shell
|
||||
curl -s http://localhost:8000/score -H "Content-Type: application/json" -d '{
|
||||
"model": "athrael-soju/colqwen3.5-4.5B",
|
||||
"text_1": "What is the capital of France?",
|
||||
"text_2": ["The capital of France is Paris.", "Python is a programming language."]
|
||||
}'
|
||||
```
|
||||
|
||||
An example can be found here: [examples/pooling/score/colqwen3_5_rerank_online.py](../../examples/pooling/score/colqwen3_5_rerank_online.py)
|
||||
|
||||
### BAAI/bge-m3
|
||||
|
||||
The `BAAI/bge-m3` model comes with extra weights for sparse and colbert embeddings but unfortunately in its `config.json`
|
||||
|
||||
@@ -834,6 +834,7 @@ The following table lists those that are tested in vLLM.
|
||||
| `CLIPModel` | CLIP | T / I | `openai/clip-vit-base-patch32`, `openai/clip-vit-large-patch14`, etc. | | |
|
||||
| `ColModernVBertForRetrieval` | ColModernVBERT | T / I | `ModernVBERT/colmodernvbert-merged` | | |
|
||||
| `ColPaliForRetrieval` | ColPali | T / I | `vidore/colpali-v1.3-hf` | | |
|
||||
| `ColQwen3_5` | ColQwen3.5 | T + I + V | `athrael-soju/colqwen3.5-4.5B-v3` | | |
|
||||
| `LlamaNemotronVLModel` | Llama Nemotron Embedding + SigLIP | T + I | `nvidia/llama-nemotron-embed-vl-1b-v2` | | |
|
||||
| `LlavaNextForConditionalGeneration`<sup>C</sup> | LLaVA-NeXT-based | T / I | `royokong/e5-v` | | ✅︎ |
|
||||
| `Phi3VForCausalLM`<sup>C</sup> | Phi-3-Vision-based | T + I | `TIGER-Lab/VLM2Vec-Full` | | ✅︎ |
|
||||
|
||||
@@ -70,6 +70,29 @@ def run_audioflamingo3(question: str, audio_count: int) -> ModelRequestData:
|
||||
)
|
||||
|
||||
|
||||
# CohereASR
|
||||
def run_cohere_asr(question: str, audio_count: int) -> ModelRequestData:
|
||||
assert audio_count == 1, "CohereASR only support single audio input per prompt"
|
||||
# TODO (ekagra): add HF ckpt after asr release
|
||||
model_name = "/host/engines/vllm/audio/2b-release"
|
||||
|
||||
prompt = (
|
||||
"<|startofcontext|><|startoftranscript|>"
|
||||
"<|emo:undefined|><|en|><|en|><|pnc|><|noitn|>"
|
||||
"<|notimestamp|><|nodiarize|>"
|
||||
)
|
||||
engine_args = EngineArgs(
|
||||
model=model_name,
|
||||
limit_mm_per_prompt={"audio": audio_count},
|
||||
trust_remote_code=True,
|
||||
)
|
||||
|
||||
return ModelRequestData(
|
||||
engine_args=engine_args,
|
||||
prompt=prompt,
|
||||
)
|
||||
|
||||
|
||||
# MusicFlamingo
|
||||
def run_musicflamingo(question: str, audio_count: int) -> ModelRequestData:
|
||||
model_name = "nvidia/music-flamingo-2601-hf"
|
||||
@@ -508,14 +531,15 @@ def run_whisper(question: str, audio_count: int) -> ModelRequestData:
|
||||
|
||||
model_example_map = {
|
||||
"audioflamingo3": run_audioflamingo3,
|
||||
"musicflamingo": run_musicflamingo,
|
||||
"cohere_asr": run_cohere_asr,
|
||||
"funaudiochat": run_funaudiochat,
|
||||
"gemma3n": run_gemma3n,
|
||||
"glmasr": run_glmasr,
|
||||
"funaudiochat": run_funaudiochat,
|
||||
"granite_speech": run_granite_speech,
|
||||
"kimi_audio": run_kimi_audio,
|
||||
"midashenglm": run_midashenglm,
|
||||
"minicpmo": run_minicpmo,
|
||||
"musicflamingo": run_musicflamingo,
|
||||
"phi4_mm": run_phi4mm,
|
||||
"qwen2_audio": run_qwen2_audio,
|
||||
"qwen2_5_omni": run_qwen2_5_omni,
|
||||
|
||||
@@ -0,0 +1,130 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
|
||||
"""
|
||||
Example of using ColQwen3.5 late interaction model for reranking.
|
||||
|
||||
ColQwen3.5 is a multi-modal ColBERT-style model based on Qwen3.5.
|
||||
It produces per-token embeddings and uses MaxSim scoring for retrieval
|
||||
and reranking. Supports both text and image inputs.
|
||||
|
||||
Start the server with:
|
||||
vllm serve athrael-soju/colqwen3.5-4.5B --max-model-len 4096
|
||||
|
||||
Then run this script:
|
||||
python colqwen3_5_rerank_online.py
|
||||
"""
|
||||
|
||||
import requests
|
||||
|
||||
MODEL = "athrael-soju/colqwen3.5-4.5B"
|
||||
BASE_URL = "http://127.0.0.1:8000"
|
||||
|
||||
headers = {"accept": "application/json", "Content-Type": "application/json"}
|
||||
|
||||
|
||||
def rerank_text():
|
||||
"""Text-only reranking via /rerank endpoint."""
|
||||
print("=" * 60)
|
||||
print("1. Text reranking (/rerank)")
|
||||
print("=" * 60)
|
||||
|
||||
data = {
|
||||
"model": MODEL,
|
||||
"query": "What is machine learning?",
|
||||
"documents": [
|
||||
"Machine learning is a subset of artificial intelligence.",
|
||||
"Python is a programming language.",
|
||||
"Deep learning uses neural networks for complex tasks.",
|
||||
"The weather today is sunny.",
|
||||
],
|
||||
}
|
||||
|
||||
response = requests.post(f"{BASE_URL}/rerank", headers=headers, json=data)
|
||||
|
||||
if response.status_code == 200:
|
||||
result = response.json()
|
||||
print("\n Ranked documents (most relevant first):")
|
||||
for item in result["results"]:
|
||||
doc_idx = item["index"]
|
||||
score = item["relevance_score"]
|
||||
print(f" [{score:.4f}] {data['documents'][doc_idx]}")
|
||||
else:
|
||||
print(f" Request failed: {response.status_code}")
|
||||
print(f" {response.text[:300]}")
|
||||
|
||||
|
||||
def score_text():
|
||||
"""Text-only scoring via /score endpoint."""
|
||||
print()
|
||||
print("=" * 60)
|
||||
print("2. Text scoring (/score)")
|
||||
print("=" * 60)
|
||||
|
||||
query = "What is the capital of France?"
|
||||
documents = [
|
||||
"The capital of France is Paris.",
|
||||
"Berlin is the capital of Germany.",
|
||||
"Python is a programming language.",
|
||||
]
|
||||
|
||||
data = {
|
||||
"model": MODEL,
|
||||
"text_1": query,
|
||||
"text_2": documents,
|
||||
}
|
||||
|
||||
response = requests.post(f"{BASE_URL}/score", headers=headers, json=data)
|
||||
|
||||
if response.status_code == 200:
|
||||
result = response.json()
|
||||
print(f"\n Query: {query}\n")
|
||||
for item in result["data"]:
|
||||
idx = item["index"]
|
||||
score = item["score"]
|
||||
print(f" Doc {idx} (score={score:.4f}): {documents[idx]}")
|
||||
else:
|
||||
print(f" Request failed: {response.status_code}")
|
||||
print(f" {response.text[:300]}")
|
||||
|
||||
|
||||
def score_text_top_n():
|
||||
"""Text reranking with top_n filtering via /rerank endpoint."""
|
||||
print()
|
||||
print("=" * 60)
|
||||
print("3. Text reranking with top_n=2 (/rerank)")
|
||||
print("=" * 60)
|
||||
|
||||
data = {
|
||||
"model": MODEL,
|
||||
"query": "What is the capital of France?",
|
||||
"documents": [
|
||||
"The capital of France is Paris.",
|
||||
"Berlin is the capital of Germany.",
|
||||
"Python is a programming language.",
|
||||
"The Eiffel Tower is in Paris.",
|
||||
],
|
||||
"top_n": 2,
|
||||
}
|
||||
|
||||
response = requests.post(f"{BASE_URL}/rerank", headers=headers, json=data)
|
||||
|
||||
if response.status_code == 200:
|
||||
result = response.json()
|
||||
print(f"\n Top {data['top_n']} results:")
|
||||
for item in result["results"]:
|
||||
doc_idx = item["index"]
|
||||
score = item["relevance_score"]
|
||||
print(f" [{score:.4f}] {data['documents'][doc_idx]}")
|
||||
else:
|
||||
print(f" Request failed: {response.status_code}")
|
||||
print(f" {response.text[:300]}")
|
||||
|
||||
|
||||
def main():
|
||||
rerank_text()
|
||||
score_text()
|
||||
score_text_top_n()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -19,8 +19,10 @@ import soundfile
|
||||
import torch
|
||||
from datasets import load_dataset
|
||||
from evaluate import load
|
||||
from transformers import AutoTokenizer
|
||||
|
||||
from vllm.tokenizers import get_tokenizer
|
||||
|
||||
from ....models.registry import HF_EXAMPLE_MODELS
|
||||
from ....utils import RemoteOpenAIServer
|
||||
|
||||
|
||||
@@ -64,8 +66,12 @@ async def bound_transcribe(sem, client, tokenizer, audio, reference):
|
||||
async def process_dataset(model, client, data, concurrent_request):
|
||||
sem = asyncio.Semaphore(concurrent_request)
|
||||
|
||||
# Load tokenizer once outside the loop
|
||||
tokenizer = AutoTokenizer.from_pretrained(model)
|
||||
model_info = HF_EXAMPLE_MODELS.find_hf_info(model)
|
||||
tokenizer = get_tokenizer(
|
||||
model,
|
||||
tokenizer_mode=model_info.tokenizer_mode,
|
||||
trust_remote_code=model_info.trust_remote_code,
|
||||
)
|
||||
|
||||
# Warmup call as the first `librosa.load` server-side is quite slow.
|
||||
audio, sr = data[0]["audio"]["array"], data[0]["audio"]["sampling_rate"]
|
||||
@@ -144,20 +150,35 @@ def run_evaluation(
|
||||
|
||||
|
||||
# alternatives "openai/whisper-large-v2", "openai/whisper-large-v3-turbo"..
|
||||
@pytest.mark.parametrize("model_name", ["openai/whisper-large-v3"])
|
||||
# NOTE: Expected WER measured with equivalent hf.transformers args:
|
||||
# whisper-large-v3 + esb-datasets-earnings22-validation-tiny-filtered.
|
||||
@pytest.mark.parametrize(
|
||||
"model_config",
|
||||
[
|
||||
("openai/whisper-large-v3", 12.744980),
|
||||
# TODO (ekagra): add HF ckpt after asr release
|
||||
# ("/host/engines/vllm/audio/2b-release", 11.73),
|
||||
],
|
||||
)
|
||||
# Original dataset is 20GB+ in size, hence we use a pre-filtered slice.
|
||||
@pytest.mark.parametrize(
|
||||
"dataset_repo", ["D4nt3/esb-datasets-earnings22-validation-tiny-filtered"]
|
||||
)
|
||||
# NOTE: Expected WER measured with equivalent hf.transformers args:
|
||||
# whisper-large-v3 + esb-datasets-earnings22-validation-tiny-filtered.
|
||||
@pytest.mark.parametrize("expected_wer", [12.744980])
|
||||
def test_wer_correctness(
|
||||
model_name, dataset_repo, expected_wer, n_examples=-1, max_concurrent_request=None
|
||||
model_config, dataset_repo, n_examples=-1, max_concurrent_request=None
|
||||
):
|
||||
model_name, expected_wer = model_config
|
||||
model_info = HF_EXAMPLE_MODELS.find_hf_info(model_name)
|
||||
# TODO refactor to use `ASRDataset`
|
||||
server_args = [
|
||||
"--enforce-eager",
|
||||
f"--tokenizer_mode={model_info.tokenizer_mode}",
|
||||
]
|
||||
if model_info.trust_remote_code:
|
||||
server_args.append("--trust-remote-code")
|
||||
with RemoteOpenAIServer(
|
||||
model_name, ["--enforce-eager"], max_wait_seconds=480
|
||||
model_name,
|
||||
server_args,
|
||||
) as remote_server:
|
||||
dataset = load_hf_dataset(dataset_repo)
|
||||
|
||||
@@ -167,7 +188,14 @@ def test_wer_correctness(
|
||||
|
||||
client = remote_server.get_async_client()
|
||||
wer = run_evaluation(
|
||||
model_name, client, dataset, max_concurrent_request, n_examples
|
||||
model_name,
|
||||
client,
|
||||
dataset,
|
||||
max_concurrent_request,
|
||||
n_examples,
|
||||
)
|
||||
|
||||
print(f"Expected WER: {expected_wer}, Actual WER: {wer}")
|
||||
|
||||
if expected_wer:
|
||||
torch.testing.assert_close(wer, expected_wer, atol=1e-1, rtol=1e-2)
|
||||
|
||||
+5
@@ -0,0 +1,5 @@
|
||||
model_name: "nvidia/NVIDIA-Nemotron-3-Nano-30B-A3B-NVFP4"
|
||||
accuracy_threshold: 0.29
|
||||
num_questions: 1319
|
||||
num_fewshot: 5
|
||||
server_args: "--enforce-eager --max-model-len 8192 --tensor-parallel-size 2 --moe-backend=cutlass"
|
||||
@@ -15,3 +15,4 @@ Mixtral-8x7B-BF16-fi-cutlass.yaml
|
||||
Mixtral-8x7B-BF16-triton.yaml
|
||||
Nemotron-Nano-30B-Fp8-ModelOpt-fi-trtllm.yaml
|
||||
Nemotron-Nano-30B-NvFp4-ModelOpt-fi-cutlass.yaml
|
||||
Nemotron-Nano-30B-NvFp4-ModelOpt-vllm-cutlass.yaml
|
||||
|
||||
@@ -0,0 +1,154 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
|
||||
"""Tests for ColQwen3.5 late interaction model for multi-modal retrieval.
|
||||
|
||||
ColQwen3.5 is a multi-vector retrieval model based on Qwen3.5 backbone with
|
||||
ColBERT-style late interaction scoring (MaxSim). It produces per-token
|
||||
embeddings for both text and image inputs.
|
||||
"""
|
||||
|
||||
import pytest
|
||||
import torch
|
||||
|
||||
from ....conftest import VllmRunner
|
||||
|
||||
MODELS = [
|
||||
"athrael-soju/colqwen3.5-4.5B-v3",
|
||||
]
|
||||
|
||||
EMBED_DIMS = {
|
||||
"athrael-soju/colqwen3.5-4.5B-v3": 320,
|
||||
}
|
||||
|
||||
TEXT_QUERIES = [
|
||||
"What is the capital of France?",
|
||||
"Describe the contents of the document.",
|
||||
]
|
||||
|
||||
TEXT_DOCUMENTS = [
|
||||
"The capital of France is Paris.",
|
||||
"This document contains important financial data.",
|
||||
]
|
||||
|
||||
DTYPE = "half"
|
||||
|
||||
|
||||
def _run_token_embed_test(
|
||||
vllm_runner: type[VllmRunner],
|
||||
model: str,
|
||||
*,
|
||||
dtype: str,
|
||||
) -> None:
|
||||
"""Verify per-token embedding shape and L2 normalization."""
|
||||
with vllm_runner(
|
||||
model,
|
||||
runner="pooling",
|
||||
dtype=dtype,
|
||||
max_model_len=4096,
|
||||
enforce_eager=True,
|
||||
) as vllm_model:
|
||||
outputs = vllm_model.token_embed([TEXT_QUERIES[0]])
|
||||
|
||||
assert len(outputs) == 1
|
||||
emb = torch.tensor(outputs[0])
|
||||
# Token embeddings should be 2D: [num_tokens, embed_dim]
|
||||
assert emb.dim() == 2
|
||||
assert emb.shape[1] == EMBED_DIMS[model]
|
||||
assert emb.shape[0] > 1
|
||||
|
||||
# Verify L2 normalization
|
||||
norms = torch.norm(emb, p=2, dim=-1)
|
||||
torch.testing.assert_close(
|
||||
norms,
|
||||
torch.ones_like(norms),
|
||||
rtol=1e-2,
|
||||
atol=1e-2,
|
||||
)
|
||||
|
||||
|
||||
def _run_late_interaction_test(
|
||||
vllm_runner: type[VllmRunner],
|
||||
model: str,
|
||||
*,
|
||||
dtype: str,
|
||||
) -> None:
|
||||
"""Verify MaxSim scoring matches manual computation."""
|
||||
from vllm.entrypoints.pooling.score.utils import compute_maxsim_score
|
||||
|
||||
with vllm_runner(
|
||||
model,
|
||||
runner="pooling",
|
||||
dtype=dtype,
|
||||
max_model_len=4096,
|
||||
enforce_eager=True,
|
||||
) as vllm_model:
|
||||
q_outputs = vllm_model.token_embed([TEXT_QUERIES[0]])
|
||||
d_outputs = vllm_model.token_embed([TEXT_DOCUMENTS[0]])
|
||||
|
||||
q_emb = torch.tensor(q_outputs[0])
|
||||
d_emb = torch.tensor(d_outputs[0])
|
||||
|
||||
manual_score = compute_maxsim_score(q_emb, d_emb).item()
|
||||
|
||||
vllm_scores = vllm_model.score(TEXT_QUERIES[0], TEXT_DOCUMENTS[0])
|
||||
|
||||
assert len(vllm_scores) == 1
|
||||
assert vllm_scores[0] == pytest.approx(manual_score, rel=0.01)
|
||||
|
||||
|
||||
def _run_relevance_test(
|
||||
vllm_runner: type[VllmRunner],
|
||||
model: str,
|
||||
*,
|
||||
dtype: str,
|
||||
) -> None:
|
||||
"""Verify that relevant documents score higher than irrelevant ones."""
|
||||
query = "What is machine learning?"
|
||||
documents = [
|
||||
"Machine learning is a subset of artificial intelligence.",
|
||||
"The weather forecast shows rain tomorrow.",
|
||||
"Deep learning uses neural networks for complex tasks.",
|
||||
]
|
||||
|
||||
with vllm_runner(
|
||||
model,
|
||||
runner="pooling",
|
||||
dtype=dtype,
|
||||
max_model_len=4096,
|
||||
enforce_eager=True,
|
||||
) as vllm_model:
|
||||
scores = vllm_model.score(query, documents)
|
||||
|
||||
assert len(scores) == 3
|
||||
assert scores[0] > scores[1], "ML doc should score higher than weather doc"
|
||||
assert scores[2] > scores[1], "DL doc should score higher than weather doc"
|
||||
|
||||
|
||||
@pytest.mark.parametrize("model", MODELS)
|
||||
@pytest.mark.parametrize("dtype", [DTYPE])
|
||||
def test_colqwen3_5_token_embed(
|
||||
vllm_runner,
|
||||
model: str,
|
||||
dtype: str,
|
||||
) -> None:
|
||||
_run_token_embed_test(vllm_runner, model, dtype=dtype)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("model", MODELS)
|
||||
@pytest.mark.parametrize("dtype", [DTYPE])
|
||||
def test_colqwen3_5_late_interaction_scoring(
|
||||
vllm_runner,
|
||||
model: str,
|
||||
dtype: str,
|
||||
) -> None:
|
||||
_run_late_interaction_test(vllm_runner, model, dtype=dtype)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("model", MODELS)
|
||||
@pytest.mark.parametrize("dtype", [DTYPE])
|
||||
def test_colqwen3_5_relevance_ordering(
|
||||
vllm_runner,
|
||||
model: str,
|
||||
dtype: str,
|
||||
) -> None:
|
||||
_run_relevance_test(vllm_runner, model, dtype=dtype)
|
||||
@@ -639,6 +639,11 @@ _LATE_INTERACTION_EXAMPLE_MODELS = {
|
||||
"OpsColQwen3Model": _HfExamplesInfo(
|
||||
"OpenSearch-AI/Ops-Colqwen3-4B", trust_remote_code=True
|
||||
),
|
||||
"ColQwen3_5": _HfExamplesInfo(
|
||||
"athrael-soju/colqwen3.5-4.5B-v3",
|
||||
trust_remote_code=True,
|
||||
max_model_len=4096,
|
||||
),
|
||||
"Qwen3VLNemotronEmbedModel": _HfExamplesInfo(
|
||||
"nvidia/nemotron-colembed-vl-4b-v2",
|
||||
),
|
||||
@@ -1116,6 +1121,11 @@ _MULTIMODAL_EXAMPLE_MODELS = {
|
||||
tokenizer_mode="mistral",
|
||||
),
|
||||
# [Encoder-decoder]
|
||||
"CohereASRForConditionalGeneration": _HfExamplesInfo(
|
||||
"/host/engines/vllm/audio/2b-release",
|
||||
trust_remote_code=True,
|
||||
is_available_online=False, # TODO (ekagra): revert after asr release
|
||||
),
|
||||
"NemotronParseForConditionalGeneration": _HfExamplesInfo(
|
||||
"nvidia/NVIDIA-Nemotron-Parse-v1.1", trust_remote_code=True
|
||||
),
|
||||
|
||||
@@ -47,7 +47,7 @@ def create_scheduler(
|
||||
enable_prefix_caching: bool = False,
|
||||
long_prefill_token_threshold: int = 0,
|
||||
disable_chunked_mm_input: bool = False,
|
||||
use_kv_connector: None | bool | MockKVConfig = None,
|
||||
use_kv_connector: None | bool | str | MockKVConfig = None,
|
||||
num_blocks: int = 10000,
|
||||
block_size: int = 16,
|
||||
max_model_len: int | None = None,
|
||||
@@ -107,6 +107,11 @@ def create_scheduler(
|
||||
"is_async": use_kv_connector.is_async,
|
||||
},
|
||||
)
|
||||
elif isinstance(use_kv_connector, str):
|
||||
kv_transfer_config = KVTransferConfig(
|
||||
kv_connector=use_kv_connector,
|
||||
kv_role="kv_both",
|
||||
)
|
||||
elif use_kv_connector:
|
||||
kv_transfer_config = KVTransferConfig(
|
||||
kv_connector="ExampleConnector",
|
||||
|
||||
@@ -0,0 +1,130 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
import pytest
|
||||
|
||||
import vllm.plugins as plugins_module
|
||||
from tests.v1.core.utils import create_requests, create_scheduler
|
||||
from vllm.distributed.kv_transfer.kv_connector.factory import (
|
||||
KVConnectorFactory,
|
||||
)
|
||||
from vllm.distributed.kv_transfer.kv_connector.v1.base import (
|
||||
KVConnectorBase_V1,
|
||||
KVConnectorMetadata,
|
||||
)
|
||||
from vllm.v1.core.kv_cache_manager import KVCacheBlocks
|
||||
from vllm.v1.core.kv_cache_utils import BlockHash
|
||||
from vllm.v1.core.sched.output import SchedulerOutput
|
||||
from vllm.v1.core.sched.scheduler import Scheduler
|
||||
from vllm.v1.request import Request
|
||||
|
||||
|
||||
class DummyConnectorMetadata(KVConnectorMetadata):
|
||||
def __init__(self, block_hashes_by_req: dict[str, list[BlockHash]]):
|
||||
self.block_hashes_by_req = block_hashes_by_req
|
||||
|
||||
|
||||
class DummyKVConnector(KVConnectorBase_V1):
|
||||
def __init__(self, vllm_config, role, kv_cache_config=None):
|
||||
super().__init__(vllm_config, role, kv_cache_config)
|
||||
|
||||
def get_num_new_matched_tokens(
|
||||
self, request: Request, num_computed_tokens: int
|
||||
) -> tuple[int | None, bool]:
|
||||
return (0, False)
|
||||
|
||||
def update_state_after_alloc(
|
||||
self, request: Request, blocks: KVCacheBlocks, num_external_tokens: int
|
||||
):
|
||||
pass
|
||||
|
||||
def build_connector_meta(
|
||||
self, scheduler_output: SchedulerOutput
|
||||
) -> KVConnectorMetadata:
|
||||
block_hashes_by_req = getattr(scheduler_output, "block_hashes_by_req", None)
|
||||
assert block_hashes_by_req is not None, (
|
||||
"DummyKVConnector expected 'block_hashes_by_req' on scheduler_output"
|
||||
)
|
||||
return DummyConnectorMetadata(
|
||||
block_hashes_by_req=block_hashes_by_req,
|
||||
)
|
||||
|
||||
def start_load_kv(self, kv_caches, finished_req_ids):
|
||||
pass
|
||||
|
||||
def wait_for_layer_load(self, layer_name):
|
||||
pass
|
||||
|
||||
def save_kv_layer(self, layer_name, kv_layer, attn_metadata, **kwargs):
|
||||
pass
|
||||
|
||||
def wait_for_save(self):
|
||||
pass
|
||||
|
||||
|
||||
def _my_plugin():
|
||||
"""Registers the dummy KV connector and overrides _build_kv_connector_meta"""
|
||||
KVConnectorFactory.register_connector(
|
||||
"DummyKVConnector",
|
||||
__name__,
|
||||
DummyKVConnector.__name__,
|
||||
)
|
||||
|
||||
def _custom_build_kv_connector_meta(
|
||||
self, connector: KVConnectorBase_V1, scheduler_output: SchedulerOutput
|
||||
) -> KVConnectorMetadata:
|
||||
block_hashes_by_req: dict[str, list[BlockHash]] = {}
|
||||
for req_id in scheduler_output.num_scheduled_tokens:
|
||||
request = self.requests[req_id]
|
||||
block_hashes_by_req[req_id] = request.block_hashes
|
||||
|
||||
scheduler_output.block_hashes_by_req = block_hashes_by_req # type: ignore[attr-defined]
|
||||
return connector.build_connector_meta(scheduler_output)
|
||||
|
||||
Scheduler._build_kv_connector_meta = _custom_build_kv_connector_meta
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def _load_plugin():
|
||||
"""Load the fake plugin through the real load_general_plugins() path."""
|
||||
ep = MagicMock()
|
||||
ep.name = "dummy_kv_connector_plugin"
|
||||
ep.value = f"{__name__}:_my_plugin"
|
||||
ep.load.return_value = _my_plugin
|
||||
|
||||
# Reset the global guard so load_general_plugins() actually runs.
|
||||
plugins_module.plugins_loaded = False
|
||||
with patch("importlib.metadata.entry_points", return_value=[ep]):
|
||||
plugins_module.load_general_plugins()
|
||||
yield
|
||||
# Reset again so other tests are not affected.
|
||||
plugins_module.plugins_loaded = False
|
||||
|
||||
|
||||
def test_connector_receives_block_hashes(_load_plugin):
|
||||
block_size = 16
|
||||
num_tokens = 48 # 3 full blocks worth of tokens
|
||||
scheduler = create_scheduler(
|
||||
use_kv_connector="DummyKVConnector", block_size=block_size
|
||||
)
|
||||
requests = create_requests(
|
||||
num_requests=3, num_tokens=num_tokens, block_size=block_size
|
||||
)
|
||||
for req in requests:
|
||||
scheduler.add_request(req)
|
||||
|
||||
output = scheduler.schedule()
|
||||
|
||||
# Verify the connector metadata was built with block hashes.
|
||||
meta = output.kv_connector_metadata
|
||||
assert isinstance(meta, DummyConnectorMetadata)
|
||||
assert len(meta.block_hashes_by_req) == 3
|
||||
|
||||
for req in requests:
|
||||
assert req.request_id in meta.block_hashes_by_req
|
||||
# Each request has num_tokens / block_size = 3 full block hashes.
|
||||
assert len(meta.block_hashes_by_req[req.request_id]) == (
|
||||
num_tokens // block_size
|
||||
)
|
||||
assert meta.block_hashes_by_req[req.request_id] == req.block_hashes
|
||||
@@ -989,6 +989,7 @@ def get_cutlass_moe_mm_data(
|
||||
n: int,
|
||||
k: int,
|
||||
blockscale_offsets: torch.Tensor | None = None,
|
||||
is_gated: bool = True,
|
||||
):
|
||||
"""
|
||||
Prepare data necessary to perform CUTLASS grouped matrix multiplications
|
||||
@@ -1012,6 +1013,8 @@ def get_cutlass_moe_mm_data(
|
||||
its computation. The number of block scale rows
|
||||
computed with expert E is blockscale_offsets[E + 1] -
|
||||
blockscale_offsets[E]
|
||||
- is_gated: Whether the activation is gated (gate + up). When True, the
|
||||
first GEMM N dimension is 2*n; when False, it is n.
|
||||
"""
|
||||
return torch.ops._C.get_cutlass_moe_mm_data(
|
||||
topk_ids,
|
||||
@@ -1024,6 +1027,7 @@ def get_cutlass_moe_mm_data(
|
||||
n,
|
||||
k,
|
||||
blockscale_offsets,
|
||||
is_gated,
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -3157,7 +3157,7 @@ class ASRDataset(HuggingFaceDataset):
|
||||
**kwargs,
|
||||
) -> list:
|
||||
output_len = output_len if output_len is not None else self.DEFAULT_OUTPUT_LEN
|
||||
if "openai" in tokenizer.name_or_path:
|
||||
if "openai" in getattr(tokenizer, "name_or_path", ""):
|
||||
prompt = "<|startoftranscript|><|en|><|transcribe|><|notimestamps|>"
|
||||
else:
|
||||
prompt = ""
|
||||
|
||||
@@ -107,7 +107,7 @@ class TranscriptionRequest(OpenAIBaseModel):
|
||||
stream_include_usage: bool | None = False
|
||||
stream_continuous_usage_stats: bool | None = False
|
||||
|
||||
vllm_xargs: dict[str, str | int | float] | None = Field(
|
||||
vllm_xargs: dict[str, str | int | float | bool] | None = Field(
|
||||
default=None,
|
||||
description=(
|
||||
"Additional request parameters with string or "
|
||||
|
||||
+6
-4
@@ -365,6 +365,7 @@ def build_enc_dec_inputs(
|
||||
encoder_inputs: SingletonInputs,
|
||||
decoder_inputs: SingletonInputs | None,
|
||||
decoder_start_token_id: int,
|
||||
skip_decoder_start_token: bool = False,
|
||||
) -> EncoderDecoderInputs:
|
||||
enc_inputs = _validate_enc_inputs(encoder_inputs)
|
||||
|
||||
@@ -396,10 +397,11 @@ def build_enc_dec_inputs(
|
||||
else:
|
||||
assert_never(enc_inputs)
|
||||
|
||||
dec_inputs_new["prompt_token_ids"] = _prepare_decoder_input_ids_for_generation(
|
||||
dec_inputs_new["prompt_token_ids"],
|
||||
decoder_start_token_id,
|
||||
)
|
||||
if not skip_decoder_start_token:
|
||||
dec_inputs_new["prompt_token_ids"] = _prepare_decoder_input_ids_for_generation(
|
||||
dec_inputs_new["prompt_token_ids"],
|
||||
decoder_start_token_id,
|
||||
)
|
||||
|
||||
if cache_salt := enc_inputs.get("cache_salt"):
|
||||
dec_inputs_new["cache_salt"] = cache_salt
|
||||
|
||||
@@ -261,6 +261,15 @@ class InputPreprocessor:
|
||||
encoder_prompt = prompt["encoder_prompt"]
|
||||
decoder_prompt = prompt["decoder_prompt"]
|
||||
|
||||
skip_decoder_start_token = False
|
||||
if self.renderer.mm_processor is not None:
|
||||
from vllm.multimodal.processing import EncDecMultiModalProcessor
|
||||
|
||||
if isinstance(self.renderer.mm_processor, EncDecMultiModalProcessor):
|
||||
skip_decoder_start_token = (
|
||||
self.renderer.mm_processor.skip_decoder_start_token
|
||||
)
|
||||
|
||||
return build_enc_dec_inputs(
|
||||
encoder_inputs=self._prompt_to_llm_inputs(
|
||||
encoder_prompt,
|
||||
@@ -275,6 +284,7 @@ class InputPreprocessor:
|
||||
)
|
||||
),
|
||||
decoder_start_token_id=self.renderer.get_dec_start_token_id(),
|
||||
skip_decoder_start_token=skip_decoder_start_token,
|
||||
)
|
||||
|
||||
def _process_decoder_only_prompt(
|
||||
|
||||
@@ -507,11 +507,12 @@ def run_cutlass_moe_fp4(
|
||||
# Gemm 1
|
||||
a: Input tensor: [m, k] (half/bfloat16)
|
||||
a1_gscale: Activation scale per expert: [e] (float32)
|
||||
w1(gate up) (not an argument to cutlass_moe_fp4): [e, 2 * n, k]
|
||||
w1_fp4: [e, 2 * n, k // 2], dtype: torch.uint8 (stacked fp4: E2M1)
|
||||
w1 (not an argument to cutlass_moe_fp4): [e, w1_n, k]
|
||||
w1_fp4: [e, w1_n, k // 2], dtype: torch.uint8 (stacked fp4: E2M1)
|
||||
where w1_n = 2*n for gated activations (gate+up), n for non-gated (up only).
|
||||
(Note: `n` is the up projection output dim, `k` is the input dim in
|
||||
full precision)
|
||||
w1_blockscale: [e, 2 * n, k // block_size] (float8_e4m3)
|
||||
w1_blockscale: [e, w1_n, k // block_size] (float8_e4m3)
|
||||
(Block size = 16 for NVFP4)
|
||||
|
||||
# Gemm 2
|
||||
@@ -528,6 +529,11 @@ def run_cutlass_moe_fp4(
|
||||
|
||||
assumes that topk < k < n to satisfy - up/down projection expectations.
|
||||
"""
|
||||
is_gated = activation.is_gated
|
||||
# For gated activations (e.g. SiLU), w1 output is 2*n (gate + up).
|
||||
# For non-gated activations (e.g. SiLU_NO_MUL), w1 output is n (up only).
|
||||
w1_n = n * 2 if is_gated else n
|
||||
|
||||
assert topk_weights.shape == topk_ids.shape, "topk shape mismatch"
|
||||
assert w1_fp4.dtype == torch.uint8, "weight 1 must be uint8"
|
||||
assert w2_fp4.dtype == torch.uint8, "weight 2 must be uint8"
|
||||
@@ -538,7 +544,7 @@ def run_cutlass_moe_fp4(
|
||||
and w2_blockscale.ndim == 3
|
||||
), "All Weights must be of rank 3 for cutlass_moe_fp4"
|
||||
m_a, k_a = a.shape
|
||||
e_w1, nx2_w1, half_k_w1 = w1_fp4.shape
|
||||
e_w1, w1_n_actual, half_k_w1 = w1_fp4.shape
|
||||
e_w2, k_w2, half_n_w2 = w2_fp4.shape
|
||||
|
||||
assert e_w1 == e_w2 and e_w1 == e, (
|
||||
@@ -548,7 +554,7 @@ def run_cutlass_moe_fp4(
|
||||
assert k_a == half_k_w1 * 2 and k == k_w2, (
|
||||
"Hidden size mismatch between a, w1 and w2"
|
||||
)
|
||||
assert nx2_w1 == n * 2 and half_n_w2 * 2 == n, "mismatch in expected `n`"
|
||||
assert w1_n_actual == w1_n and half_n_w2 * 2 == n, "mismatch in expected `n`"
|
||||
assert m == m_a, "input shape mismatch"
|
||||
assert 2 * half_k_w1 == k_w2, "Hidden size mismatch w2 and w1"
|
||||
assert a.dtype in [torch.half, torch.bfloat16], "Invalid input dtype"
|
||||
@@ -589,6 +595,7 @@ def run_cutlass_moe_fp4(
|
||||
n,
|
||||
k,
|
||||
blockscale_offsets,
|
||||
is_gated=is_gated,
|
||||
)
|
||||
|
||||
a = ops.shuffle_rows(a, a_map)
|
||||
@@ -599,7 +606,7 @@ def run_cutlass_moe_fp4(
|
||||
blockscale_offsets,
|
||||
num_topk,
|
||||
)
|
||||
c1 = _resize_cache(workspace13, (m * topk, n * 2))
|
||||
c1 = _resize_cache(workspace13, (m * topk, w1_n))
|
||||
c2 = _resize_cache(workspace2, (m * topk, n))
|
||||
c3 = _resize_cache(workspace13, (m * topk, k))
|
||||
ops.cutlass_fp4_moe_mm(
|
||||
@@ -681,7 +688,7 @@ class CutlassExpertsFp4(mk.FusedMoEExpertsModular):
|
||||
|
||||
@staticmethod
|
||||
def _supports_no_act_and_mul() -> bool:
|
||||
return False
|
||||
return True
|
||||
|
||||
@staticmethod
|
||||
def _supports_quant_scheme(
|
||||
@@ -695,11 +702,16 @@ class CutlassExpertsFp4(mk.FusedMoEExpertsModular):
|
||||
# SILU uses a fused silu+mul+fp4_quant kernel path.
|
||||
# Other gated activations use the generic apply_moe_activation()
|
||||
# fallback + separate fp4 quantization in run_cutlass_moe_fp4().
|
||||
# Non-gated activations (_NO_MUL) are also supported for models
|
||||
# like Nemotron-Nano that don't use gated MLP.
|
||||
return activation in [
|
||||
MoEActivation.SILU,
|
||||
MoEActivation.GELU,
|
||||
MoEActivation.SWIGLUOAI,
|
||||
MoEActivation.SWIGLUSTEP,
|
||||
MoEActivation.SILU_NO_MUL,
|
||||
MoEActivation.GELU_NO_MUL,
|
||||
MoEActivation.RELU2_NO_MUL,
|
||||
]
|
||||
|
||||
@staticmethod
|
||||
|
||||
@@ -856,8 +856,12 @@ def safetensors_weights_iterator(
|
||||
for name in f.keys(): # noqa: SIM118
|
||||
if should_skip_weight(name, local_expert_ids):
|
||||
continue
|
||||
param = f.get_tensor(name)
|
||||
yield name, param
|
||||
# Yield a SafetensorsSlice that defers the disk
|
||||
# read. Downstream weight loaders call .narrow()
|
||||
# to select only their TP shard; the actual I/O
|
||||
# happens at .materialize() time so we never read
|
||||
# data that will be discarded.
|
||||
yield name, SafetensorsSlice(f.get_slice(name))
|
||||
|
||||
|
||||
def multi_thread_safetensors_weights_iterator(
|
||||
@@ -1155,6 +1159,141 @@ def gguf_quant_weights_iterator(
|
||||
yield name, param
|
||||
|
||||
|
||||
class SafetensorsSlice:
|
||||
"""Lazy wrapper around a safetensors ``PySafeSlice`` for deferred,
|
||||
partial disk reads.
|
||||
|
||||
When tensor-parallel weight loading is active, each rank only needs a
|
||||
*shard* of most weight tensors. Without this wrapper every rank reads
|
||||
the **full** tensor from disk and then calls ``torch.Tensor.narrow()``
|
||||
to extract its shard, throwing the rest away.
|
||||
|
||||
``SafetensorsSlice`` intercepts ``.narrow()`` calls and records them
|
||||
without touching the disk. The actual (partial) read happens only when
|
||||
``.materialize()`` is called – or when the object is used in a context
|
||||
that requires a real tensor (e.g. ``.reshape()``, ``.item()``,
|
||||
``param_data.copy_(lazy_weight)``).
|
||||
|
||||
Because the safetensors format stores tensors contiguously, slicing on
|
||||
the outer-most dimension (dim 0) translates to a contiguous sub-read
|
||||
and avoids pulling the full tensor into host memory.
|
||||
"""
|
||||
|
||||
__slots__ = ("_slice", "_ndim", "_dims")
|
||||
|
||||
def __init__(self, safe_slice: Any) -> None:
|
||||
original_shape = safe_slice.get_shape()
|
||||
self._slice = safe_slice
|
||||
self._ndim = len(original_shape)
|
||||
# Per-dimension bookkeeping: (absolute_offset, current_size)
|
||||
self._dims: list[tuple[int, int]] = [(0, s) for s in original_shape]
|
||||
|
||||
# -- Tensor-like API used by weight loaders --------------------------
|
||||
|
||||
@property
|
||||
def shape(self) -> torch.Size:
|
||||
return torch.Size(s for _, s in self._dims)
|
||||
|
||||
def size(self, dim: int | None = None): # type: ignore[override]
|
||||
if dim is not None:
|
||||
return self._dims[dim][1]
|
||||
return self.shape
|
||||
|
||||
def numel(self) -> int:
|
||||
result = 1
|
||||
for _, s in self._dims:
|
||||
result *= s
|
||||
return result
|
||||
|
||||
def __len__(self) -> int:
|
||||
return self._dims[0][1]
|
||||
|
||||
def dim(self) -> int:
|
||||
return self._ndim
|
||||
|
||||
# -- Lazy operations (no disk I/O) -----------------------------------
|
||||
|
||||
def narrow(self, dim: int, start: int, length: int) -> "SafetensorsSlice":
|
||||
"""Return a new slice narrowed on *dim* – zero-cost, no I/O."""
|
||||
offset, size = self._dims[dim]
|
||||
assert start + length <= size, (
|
||||
f"narrow(dim={dim}, start={start}, length={length}) "
|
||||
f"out of bounds for size {size}"
|
||||
)
|
||||
new = SafetensorsSlice.__new__(SafetensorsSlice)
|
||||
new._slice = self._slice
|
||||
new._ndim = self._ndim
|
||||
new._dims = list(self._dims)
|
||||
new._dims[dim] = (offset + start, length)
|
||||
return new
|
||||
|
||||
# -- Materialisation (disk I/O happens here) -------------------------
|
||||
|
||||
def materialize(self) -> torch.Tensor:
|
||||
"""Read only the recorded sub-region from disk."""
|
||||
indexing = tuple(slice(offset, offset + size) for offset, size in self._dims)
|
||||
return self._slice[indexing]
|
||||
|
||||
# -- Fallback: auto-materialise for ops we cannot defer ---------------
|
||||
|
||||
def reshape(self, *args: Any) -> torch.Tensor:
|
||||
return self.materialize().reshape(*args)
|
||||
|
||||
def view(self, *args: Any) -> torch.Tensor:
|
||||
return self.materialize().view(*args)
|
||||
|
||||
def t(self) -> torch.Tensor:
|
||||
return self.materialize().t()
|
||||
|
||||
def item(self) -> Any:
|
||||
return self.materialize().item()
|
||||
|
||||
def to(self, *args: Any, **kwargs: Any) -> torch.Tensor:
|
||||
return self.materialize().to(*args, **kwargs)
|
||||
|
||||
@property
|
||||
def dtype(self) -> torch.dtype:
|
||||
return self.materialize().dtype
|
||||
|
||||
def __getitem__(self, key: Any) -> torch.Tensor:
|
||||
return self.materialize().__getitem__(key)
|
||||
|
||||
@classmethod
|
||||
def __torch_function__(cls, func, types, args=(), kwargs=None):
|
||||
"""Auto-materialise when used as an argument to any torch operation.
|
||||
|
||||
This ensures that ``param_data.copy_(safetensors_slice)`` works
|
||||
transparently in all weight loaders without requiring any
|
||||
downstream code changes.
|
||||
|
||||
The ``torch.narrow`` / ``torch.Tensor.narrow`` case is kept lazy
|
||||
so that TP shard extraction still avoids unnecessary I/O.
|
||||
"""
|
||||
kwargs = kwargs or {}
|
||||
# Keep narrow lazy – delegate to the instance method.
|
||||
if func is torch.narrow or func is torch.Tensor.narrow:
|
||||
self_arg = args[0]
|
||||
if isinstance(self_arg, SafetensorsSlice):
|
||||
return self_arg.narrow(*args[1:], **(kwargs or {}))
|
||||
new_args = tuple(
|
||||
a.materialize() if isinstance(a, SafetensorsSlice) else a for a in args
|
||||
)
|
||||
return func(*new_args, **kwargs)
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return f"SafetensorsSlice(shape={list(self.shape)}, dims={self._dims})"
|
||||
|
||||
|
||||
def materialize_weight(
|
||||
weight: "torch.Tensor | SafetensorsSlice",
|
||||
) -> torch.Tensor:
|
||||
"""Materialise a ``SafetensorsSlice`` into a real tensor (no-op on
|
||||
tensors that are already materialised)."""
|
||||
if isinstance(weight, SafetensorsSlice):
|
||||
return weight.materialize()
|
||||
return weight
|
||||
|
||||
|
||||
def convert_pyslice_to_tensor(x: Any) -> torch.Tensor:
|
||||
"""convert PySafeSlice object from safetensors to torch.Tensor
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,246 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
|
||||
"""
|
||||
ColQwen3.5 late interaction model for multi-modal retrieval and reranking.
|
||||
|
||||
ColQwen3.5 extends Qwen3.5 with a ColBERT-style late interaction head,
|
||||
producing per-token embeddings for both text and image inputs. It uses
|
||||
MaxSim scoring for retrieval/reranking tasks.
|
||||
|
||||
This model supports the "token_embed" pooling task and is designed for
|
||||
multi-vector retrieval of documents containing both text and images.
|
||||
|
||||
Reference: https://arxiv.org/abs/2407.01449 (ColPali)
|
||||
Based on: Qwen3.5 backbone with custom text projection
|
||||
|
||||
Target models:
|
||||
- athrael-soju/colqwen3.5-4.5B-v3
|
||||
"""
|
||||
|
||||
from collections.abc import Iterable, Mapping
|
||||
|
||||
import torch
|
||||
import torch.nn as nn
|
||||
from transformers.models.qwen3_vl import Qwen3VLProcessor
|
||||
|
||||
from vllm.config import VllmConfig
|
||||
from vllm.model_executor.layers.pooler.tokwise import pooler_for_token_embed
|
||||
from vllm.model_executor.model_loader.weight_utils import default_weight_loader
|
||||
from vllm.multimodal import MULTIMODAL_REGISTRY
|
||||
|
||||
from .interfaces import SupportsLateInteraction
|
||||
from .interfaces_base import default_pooling_type
|
||||
from .qwen2_vl import Qwen2VLMultiModalDataParser
|
||||
from .qwen3_5 import (
|
||||
Qwen3_5ForConditionalGeneration,
|
||||
Qwen3_5ProcessingInfo,
|
||||
)
|
||||
from .qwen3_vl import (
|
||||
Qwen3VLDummyInputsBuilder,
|
||||
Qwen3VLMultiModalProcessor,
|
||||
)
|
||||
from .utils import AutoWeightsLoader, WeightsMapper
|
||||
|
||||
|
||||
class ColQwen3_5ProcessingInfo(Qwen3_5ProcessingInfo):
|
||||
"""Processing info for ColQwen3.5 models.
|
||||
|
||||
ColQwen3.5 models use custom HuggingFace processors (e.g.
|
||||
ColQwen3_5Processor) that are incompatible with vLLM's
|
||||
Qwen3VLMultiModalProcessor. We override get_hf_config() and
|
||||
get_hf_processor() to skip the strict type check and force the
|
||||
standard Qwen3VLProcessor.
|
||||
"""
|
||||
|
||||
def get_hf_config(self):
|
||||
return self.ctx.get_hf_config()
|
||||
|
||||
def get_hf_processor(self, **kwargs: object) -> Qwen3VLProcessor:
|
||||
return self.ctx.get_hf_processor(
|
||||
Qwen3VLProcessor,
|
||||
use_fast=kwargs.pop("use_fast", True),
|
||||
**kwargs,
|
||||
)
|
||||
|
||||
@property
|
||||
def _supports_video(self) -> bool:
|
||||
"""Check if the HF processor supports video inputs."""
|
||||
return hasattr(self.get_hf_processor(), "video_processor")
|
||||
|
||||
def get_video_processor(self, **kwargs: object):
|
||||
if not self._supports_video:
|
||||
raise AttributeError(
|
||||
f"The processor for {self.ctx.model_config.model} does not "
|
||||
"support video inputs (no video_processor attribute)."
|
||||
)
|
||||
return self.get_hf_processor(**kwargs).video_processor # type: ignore[attr-defined]
|
||||
|
||||
def get_supported_mm_limits(self) -> Mapping[str, int | None]:
|
||||
limits: dict[str, int | None] = {"image": None}
|
||||
if self._supports_video:
|
||||
limits["video"] = None
|
||||
return limits
|
||||
|
||||
def get_mm_max_tokens_per_item(
|
||||
self,
|
||||
seq_len: int,
|
||||
mm_counts: Mapping[str, int],
|
||||
) -> Mapping[str, int]:
|
||||
max_image_tokens = self.get_max_image_tokens()
|
||||
result: dict[str, int] = {"image": max_image_tokens}
|
||||
if self._supports_video:
|
||||
max_video_tokens = self.get_max_video_tokens(seq_len, mm_counts)
|
||||
result["video"] = max_video_tokens
|
||||
return result
|
||||
|
||||
def get_data_parser(self):
|
||||
hf_config = self.get_hf_config()
|
||||
spatial_merge_size = hf_config.vision_config.spatial_merge_size
|
||||
return Qwen2VLMultiModalDataParser(
|
||||
spatial_merge_size,
|
||||
video_needs_metadata=self._supports_video,
|
||||
expected_hidden_size=self._get_expected_hidden_size(),
|
||||
)
|
||||
|
||||
|
||||
@default_pooling_type(seq_pooling_type="CLS", tok_pooling_type="ALL")
|
||||
@MULTIMODAL_REGISTRY.register_processor(
|
||||
Qwen3VLMultiModalProcessor,
|
||||
info=ColQwen3_5ProcessingInfo,
|
||||
dummy_inputs=Qwen3VLDummyInputsBuilder,
|
||||
)
|
||||
class ColQwen3_5Model(
|
||||
Qwen3_5ForConditionalGeneration,
|
||||
SupportsLateInteraction,
|
||||
):
|
||||
"""ColQwen3.5 late interaction model for multi-modal retrieval/reranking.
|
||||
|
||||
This model extends Qwen3_5ForConditionalGeneration with a ColBERT-style
|
||||
linear projection layer for per-token embeddings. It supports:
|
||||
- "token_embed" task: Per-token embeddings for late interaction scoring
|
||||
|
||||
The model produces per-token embeddings by:
|
||||
1. Running the Qwen3.5 backbone (vision + language) to get hidden states
|
||||
2. Projecting hidden states through a linear layer (hidden_size -> embed_dim)
|
||||
3. L2 normalization is handled by the pooler via PoolerNormalize
|
||||
|
||||
Attributes:
|
||||
custom_text_proj: Linear projection from hidden_size to embed_dim
|
||||
"""
|
||||
|
||||
# Mark this as a pooling model so vLLM routes to pooler path
|
||||
is_pooling_model = True
|
||||
|
||||
# Override hf_to_vllm_mapper to handle ColQwen3.5 weight naming.
|
||||
# ColPali saves weights as "language_model.*" but vLLM's
|
||||
# Qwen3_5ForCausalLM has them under "language_model.model.*".
|
||||
# Visual weights ("visual.*") already match the vLLM module path.
|
||||
hf_to_vllm_mapper = WeightsMapper(
|
||||
orig_to_new_prefix={
|
||||
"language_model.": "language_model.model.",
|
||||
}
|
||||
)
|
||||
|
||||
def __init__(self, *, vllm_config: VllmConfig, prefix: str = ""):
|
||||
super().__init__(vllm_config=vllm_config, prefix=prefix)
|
||||
|
||||
config = vllm_config.model_config.hf_config
|
||||
head_dtype = vllm_config.model_config.head_dtype
|
||||
|
||||
hidden_size = getattr(config, "hidden_size", None)
|
||||
if hidden_size is None and hasattr(config, "text_config"):
|
||||
hidden_size = config.text_config.hidden_size
|
||||
if hidden_size is None:
|
||||
raise ValueError(
|
||||
"Unable to determine text hidden size from config. "
|
||||
"Expected 'hidden_size' or 'text_config.hidden_size'."
|
||||
)
|
||||
|
||||
# (ColPali: dim, projection_dim, colbert_dim)
|
||||
self.embed_dim: int = (
|
||||
getattr(config, "embed_dim", None)
|
||||
or getattr(config, "dims", None)
|
||||
or getattr(config, "dim", None)
|
||||
or getattr(config, "projection_dim", None)
|
||||
or getattr(config, "colbert_dim", None)
|
||||
or 128 # default from reference implementation
|
||||
)
|
||||
|
||||
self.custom_text_proj = nn.Linear(
|
||||
hidden_size,
|
||||
self.embed_dim,
|
||||
bias=False,
|
||||
dtype=head_dtype,
|
||||
)
|
||||
|
||||
pooler_config = vllm_config.model_config.pooler_config
|
||||
assert pooler_config is not None
|
||||
self.pooler = pooler_for_token_embed(
|
||||
pooler_config,
|
||||
projector=None,
|
||||
)
|
||||
|
||||
def forward(
|
||||
self,
|
||||
input_ids: torch.Tensor | None,
|
||||
positions: torch.Tensor,
|
||||
intermediate_tensors=None,
|
||||
inputs_embeds: torch.Tensor | None = None,
|
||||
**kwargs: object,
|
||||
) -> torch.Tensor:
|
||||
"""Run forward pass producing per-token embeddings."""
|
||||
hidden_states = super().forward(
|
||||
input_ids=input_ids,
|
||||
positions=positions,
|
||||
intermediate_tensors=intermediate_tensors,
|
||||
inputs_embeds=inputs_embeds,
|
||||
**kwargs,
|
||||
)
|
||||
|
||||
if not isinstance(hidden_states, torch.Tensor):
|
||||
return hidden_states # type: ignore
|
||||
|
||||
proj_dtype = self.custom_text_proj.weight.dtype
|
||||
if hidden_states.dtype != proj_dtype:
|
||||
hidden_states = hidden_states.to(proj_dtype)
|
||||
|
||||
# Project to embedding dimension (normalization handled by pooler)
|
||||
return self.custom_text_proj(hidden_states)
|
||||
|
||||
# Names used for the projection layer across different ColQwen3.5 variants
|
||||
_PROJ_LAYER_NAMES = {
|
||||
"custom_text_proj", # ColPali naming
|
||||
"embedding_proj_layer", # Alternative naming
|
||||
}
|
||||
|
||||
def _is_proj_weight(self, name: str) -> bool:
|
||||
"""Check if a weight name belongs to the projection layer."""
|
||||
return any(proj_name in name for proj_name in self._PROJ_LAYER_NAMES)
|
||||
|
||||
def load_weights(self, weights: Iterable[tuple[str, torch.Tensor]]) -> set[str]:
|
||||
"""Load weights with special handling for projection layer."""
|
||||
weights_list = list(weights)
|
||||
proj_weights: list[tuple[str, torch.Tensor]] = []
|
||||
model_weights: list[tuple[str, torch.Tensor]] = []
|
||||
|
||||
for name, weight in weights_list:
|
||||
if self._is_proj_weight(name):
|
||||
proj_weights.append((name, weight))
|
||||
else:
|
||||
model_weights.append((name, weight))
|
||||
|
||||
loader = AutoWeightsLoader(
|
||||
self,
|
||||
skip_prefixes=["mtp."],
|
||||
)
|
||||
loaded = loader.load_weights(model_weights, mapper=self.hf_to_vllm_mapper)
|
||||
|
||||
for name, weight in proj_weights:
|
||||
param_name = name.split(".")[-1]
|
||||
param = getattr(self.custom_text_proj, param_name, None)
|
||||
if param is not None:
|
||||
weight = weight.to(device=param.device, dtype=param.dtype)
|
||||
default_weight_loader(param, weight)
|
||||
loaded.add(f"custom_text_proj.{param_name}")
|
||||
|
||||
return loaded
|
||||
@@ -647,6 +647,7 @@ class VoyageQwen3BidirectionalEmbedModelConfig(VerifyAndUpdateConfig):
|
||||
|
||||
MODELS_CONFIG_MAP: dict[str, type[VerifyAndUpdateConfig]] = {
|
||||
"ColBERTJinaRobertaModel": JinaRobertaModelConfig,
|
||||
"ColQwen3_5": Qwen3_5ForConditionalGenerationConfig,
|
||||
"DeepseekV32ForCausalLM": DeepseekV32ForCausalLM,
|
||||
"Ernie4_5_VLMoeForConditionalGeneration": Ernie4_5_VLMoeForConditionalGenerationConfig, # noqa: E501
|
||||
"FalconMambaForCausalLM": MambaModelConfig,
|
||||
|
||||
@@ -274,8 +274,10 @@ _LATE_INTERACTION_MODELS = {
|
||||
"ColBERTJinaRobertaModel": ("colbert", "ColBERTJinaRobertaModel"),
|
||||
# [Multimodal]
|
||||
"ColModernVBertForRetrieval": ("colmodernvbert", "ColModernVBertForRetrieval"),
|
||||
"ColPaliForRetrieval": ("colpali", "ColPaliModel"),
|
||||
"ColQwen3": ("colqwen3", "ColQwen3Model"),
|
||||
"OpsColQwen3Model": ("colqwen3", "ColQwen3Model"),
|
||||
"ColQwen3_5": ("colqwen3_5", "ColQwen3_5Model"),
|
||||
"Qwen3VLNemotronEmbedModel": ("colqwen3", "ColQwen3Model"),
|
||||
}
|
||||
|
||||
@@ -534,6 +536,10 @@ _MULTIMODAL_MODELS = {
|
||||
"VoxtralForConditionalGeneration": ("voxtral", "VoxtralForConditionalGeneration"), # noqa: E501
|
||||
"VoxtralRealtimeGeneration": ("voxtral_realtime", "VoxtralRealtimeGeneration"), # noqa: E501
|
||||
# [Encoder-decoder]
|
||||
"CohereASRForConditionalGeneration": (
|
||||
"cohere_asr",
|
||||
"CohereASRForConditionalGeneration",
|
||||
),
|
||||
"NemotronParseForConditionalGeneration": (
|
||||
"nemotron_parse",
|
||||
"NemotronParseForConditionalGeneration",
|
||||
|
||||
@@ -1682,6 +1682,8 @@ class BaseMultiModalProcessor(ABC, Generic[_I]):
|
||||
|
||||
|
||||
class EncDecMultiModalProcessor(BaseMultiModalProcessor[_I]):
|
||||
skip_decoder_start_token: bool = False
|
||||
|
||||
@abstractmethod
|
||||
def create_encoder_prompt(
|
||||
self,
|
||||
|
||||
@@ -438,8 +438,6 @@ class RocmPlatform(Platform):
|
||||
device_capability = cls.get_device_capability()
|
||||
assert device_capability is not None
|
||||
|
||||
attn_selector_config = attn_selector_config._replace(block_size=None)
|
||||
|
||||
# First try checking just the selected backend, if there is one.
|
||||
if selected_backend is not None:
|
||||
try:
|
||||
|
||||
@@ -700,12 +700,20 @@ class BaseRenderer(ABC, Generic[_T]):
|
||||
enc_prompt = prompt["encoder_prompt"]
|
||||
dec_prompt = prompt["decoder_prompt"]
|
||||
|
||||
skip_decoder_start_token = False
|
||||
if self.mm_processor is not None:
|
||||
from vllm.multimodal.processing import EncDecMultiModalProcessor
|
||||
|
||||
if isinstance(self.mm_processor, EncDecMultiModalProcessor):
|
||||
skip_decoder_start_token = self.mm_processor.skip_decoder_start_token
|
||||
|
||||
return build_enc_dec_inputs(
|
||||
encoder_inputs=self._process_singleton(enc_prompt),
|
||||
decoder_inputs=(
|
||||
None if dec_prompt is None else self._process_singleton(dec_prompt)
|
||||
),
|
||||
decoder_start_token_id=self.get_dec_start_token_id(),
|
||||
skip_decoder_start_token=skip_decoder_start_token,
|
||||
)
|
||||
|
||||
def process_for_engine(
|
||||
|
||||
@@ -300,6 +300,28 @@ class ModelArchConfigConvertorBase:
|
||||
return model_arch_config
|
||||
|
||||
|
||||
class CohereAsrModelArchConfigConvertor(ModelArchConfigConvertorBase):
|
||||
def get_total_num_attention_heads(self) -> int:
|
||||
return self.hf_text_config.transf_decoder["config_dict"]["num_attention_heads"]
|
||||
|
||||
def get_head_size(self) -> int:
|
||||
hidden_size = self.hf_text_config.transf_decoder["config_dict"]["hidden_size"]
|
||||
num_attention_heads = self.hf_text_config.transf_decoder["config_dict"][
|
||||
"num_attention_heads"
|
||||
]
|
||||
return hidden_size // num_attention_heads
|
||||
|
||||
def get_total_num_kv_heads(self) -> int:
|
||||
enc_num_kv_heads = self.hf_text_config.encoder["n_heads"]
|
||||
dec_num_kv_heads = self.hf_text_config.transf_decoder["config_dict"][
|
||||
"num_attention_heads"
|
||||
]
|
||||
assert enc_num_kv_heads == dec_num_kv_heads, (
|
||||
"Encoder and decoder must have the same number of kv heads"
|
||||
)
|
||||
return enc_num_kv_heads
|
||||
|
||||
|
||||
class MambaModelArchConfigConvertor(ModelArchConfigConvertorBase):
|
||||
def get_head_size(self) -> int:
|
||||
return 0
|
||||
@@ -425,6 +447,7 @@ class LongCatFlashMTPModelArchConfigConvertor(ModelArchConfigConvertorBase):
|
||||
|
||||
# hf_config.model_type -> convertor class
|
||||
MODEL_ARCH_CONFIG_CONVERTORS = {
|
||||
"cohere_asr": CohereAsrModelArchConfigConvertor,
|
||||
"mamba": MambaModelArchConfigConvertor,
|
||||
"falcon_mamba": MambaModelArchConfigConvertor,
|
||||
"timm_wrapper": TerratorchModelArchConfigConvertor,
|
||||
|
||||
@@ -12,6 +12,7 @@ import importlib
|
||||
|
||||
__all__ = [
|
||||
"BagelProcessor",
|
||||
"CohereASRProcessor",
|
||||
"DeepseekVLV2Processor",
|
||||
"Eagle2_5_VLProcessor",
|
||||
"FireRedASR2Processor",
|
||||
@@ -38,6 +39,7 @@ __all__ = [
|
||||
|
||||
_CLASS_TO_MODULE: dict[str, str] = {
|
||||
"BagelProcessor": "vllm.transformers_utils.processors.bagel",
|
||||
"CohereASRProcessor": "vllm.transformers_utils.processors.cohere_asr",
|
||||
"DeepseekVLV2Processor": "vllm.transformers_utils.processors.deepseek_vl2",
|
||||
"Eagle2_5_VLProcessor": "vllm.transformers_utils.processors.eagle2_5_vl",
|
||||
"FireRedASR2Processor": "vllm.transformers_utils.processors.fireredasr2",
|
||||
|
||||
@@ -0,0 +1,575 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
|
||||
import logging
|
||||
import math
|
||||
import random
|
||||
|
||||
import librosa
|
||||
import numpy as np
|
||||
import torch
|
||||
import torch.nn.functional as F
|
||||
from torch import nn
|
||||
from transformers import AutoFeatureExtractor, AutoProcessor, BatchFeature
|
||||
from transformers.feature_extraction_sequence_utils import (
|
||||
SequenceFeatureExtractor,
|
||||
)
|
||||
from transformers.processing_utils import ProcessorMixin
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
CONSTANT = 1e-5
|
||||
INF_VAL = 10000.0
|
||||
|
||||
|
||||
class FilterbankFeatures(nn.Module):
|
||||
"""Featurizer that converts wavs to Mel Spectrograms.
|
||||
See AudioToMelSpectrogramPreprocessor for args.
|
||||
"""
|
||||
|
||||
window: torch.Tensor
|
||||
fb: torch.Tensor
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
sample_rate=16000,
|
||||
n_window_size=320,
|
||||
n_window_stride=160,
|
||||
window="hann",
|
||||
normalize="per_feature",
|
||||
n_fft=None,
|
||||
preemph=0.97,
|
||||
nfilt=64,
|
||||
lowfreq=0,
|
||||
highfreq=None,
|
||||
log=True,
|
||||
log_zero_guard_type="add",
|
||||
log_zero_guard_value=2**-24,
|
||||
dither=CONSTANT,
|
||||
pad_to=16,
|
||||
max_duration=30,
|
||||
frame_splicing=1,
|
||||
exact_pad=False,
|
||||
pad_value=0,
|
||||
mag_power=2.0,
|
||||
use_grads=False,
|
||||
rng=None,
|
||||
nb_augmentation_prob=0.0,
|
||||
nb_max_freq=4000,
|
||||
mel_norm="slaney",
|
||||
stft_exact_pad=False,
|
||||
stft_conv=False,
|
||||
device="cpu",
|
||||
):
|
||||
super().__init__()
|
||||
if stft_conv or stft_exact_pad:
|
||||
logger.warning(
|
||||
"Using torch_stft is deprecated and has been removed. "
|
||||
"The values have been forcibly set to False for "
|
||||
"FilterbankFeatures and AudioToMelSpectrogramPreprocessor. "
|
||||
"Please set exact_pad to True as needed."
|
||||
)
|
||||
if exact_pad and n_window_stride % 2 == 1:
|
||||
raise NotImplementedError(
|
||||
f"{self} received exact_pad == True, but hop_size was odd. "
|
||||
"If audio_length % hop_size == 0, the returned spectrogram "
|
||||
"would not be of length audio_length // hop_size. "
|
||||
"Please use an even hop_size."
|
||||
)
|
||||
self.log_zero_guard_value = log_zero_guard_value
|
||||
if (
|
||||
n_window_size is None
|
||||
or n_window_stride is None
|
||||
or not isinstance(n_window_size, int)
|
||||
or not isinstance(n_window_stride, int)
|
||||
or n_window_size <= 0
|
||||
or n_window_stride <= 0
|
||||
):
|
||||
raise ValueError(
|
||||
f"{self} got an invalid value for either n_window_size or "
|
||||
f"n_window_stride. Both must be positive ints."
|
||||
)
|
||||
|
||||
self.sample_rate = sample_rate
|
||||
self.win_length = n_window_size
|
||||
self.hop_length = n_window_stride
|
||||
self.n_fft = n_fft or 2 ** math.ceil(math.log2(self.win_length))
|
||||
self.stft_pad_amount = (
|
||||
(self.n_fft - self.hop_length) // 2 if exact_pad else None
|
||||
)
|
||||
self.exact_pad = exact_pad
|
||||
self.sample_rate = sample_rate
|
||||
self.max_duration = max_duration
|
||||
|
||||
if exact_pad:
|
||||
logger.info("STFT using exact pad")
|
||||
torch_windows = {
|
||||
"hann": torch.hann_window,
|
||||
"hamming": torch.hamming_window,
|
||||
"blackman": torch.blackman_window,
|
||||
"bartlett": torch.bartlett_window,
|
||||
"none": None,
|
||||
}
|
||||
window_fn = torch_windows.get(window)
|
||||
window_tensor = (
|
||||
window_fn(self.win_length, periodic=False) if window_fn else None
|
||||
)
|
||||
self.register_buffer("window", window_tensor)
|
||||
|
||||
self.normalize = normalize
|
||||
self.log = log
|
||||
self.dither = dither
|
||||
self.frame_splicing = frame_splicing
|
||||
self.nfilt = nfilt
|
||||
self.preemph = preemph
|
||||
self.pad_to = pad_to
|
||||
highfreq = highfreq or sample_rate / 2
|
||||
self.sample_rate = sample_rate
|
||||
# disable pad min duration
|
||||
# self.pad_min_duration = 1.0
|
||||
self.pad_min_duration = 0.0
|
||||
self.pad_direction = "both"
|
||||
|
||||
filterbanks = torch.tensor(
|
||||
librosa.filters.mel(
|
||||
sr=sample_rate,
|
||||
n_fft=self.n_fft,
|
||||
n_mels=nfilt,
|
||||
fmin=lowfreq,
|
||||
fmax=highfreq,
|
||||
norm=mel_norm,
|
||||
),
|
||||
dtype=torch.float,
|
||||
).unsqueeze(0)
|
||||
self.register_buffer("fb", filterbanks)
|
||||
|
||||
# Calculate maximum sequence length
|
||||
max_length = self.get_seq_len(
|
||||
torch.tensor(max_duration * sample_rate, dtype=torch.float)
|
||||
)
|
||||
max_pad = pad_to - (max_length % pad_to) if pad_to > 0 else 0
|
||||
self.max_length = max_length + max_pad
|
||||
self.pad_value = pad_value
|
||||
self.mag_power = mag_power
|
||||
|
||||
# We want to avoid taking the log of zero
|
||||
# There are two options: either adding or clamping to a small value
|
||||
if log_zero_guard_type not in ["add", "clamp"]:
|
||||
raise ValueError(
|
||||
f"{self} received {log_zero_guard_type} for the "
|
||||
f"log_zero_guard_type parameter. It must be either 'add' or "
|
||||
f"'clamp'."
|
||||
)
|
||||
|
||||
self.use_grads = use_grads
|
||||
if not use_grads:
|
||||
self.forward = torch.no_grad()(self.forward)
|
||||
self._rng = random.Random() if rng is None else rng
|
||||
self.nb_augmentation_prob = nb_augmentation_prob
|
||||
if self.nb_augmentation_prob > 0.0:
|
||||
if nb_max_freq >= sample_rate / 2:
|
||||
self.nb_augmentation_prob = 0.0
|
||||
else:
|
||||
self._nb_max_fft_bin = int((nb_max_freq / sample_rate) * n_fft)
|
||||
|
||||
# log_zero_guard_value is the the small we want to use, we support
|
||||
# an actual number, or "tiny", or "eps"
|
||||
self.log_zero_guard_type = log_zero_guard_type
|
||||
|
||||
assert self.window is not None
|
||||
assert self.fb is not None
|
||||
self.window = self.window.to(dtype=torch.bfloat16)
|
||||
self.fb = self.fb.to(dtype=torch.bfloat16)
|
||||
|
||||
self.generator = torch.Generator(device=device)
|
||||
self.generator.manual_seed(0)
|
||||
|
||||
@torch._dynamo.disable
|
||||
def stft(self, x):
|
||||
# disable autocast to get full range of stft values
|
||||
with torch.amp.autocast(x.device.type, enabled=False):
|
||||
return torch.stft(
|
||||
x,
|
||||
n_fft=self.n_fft,
|
||||
hop_length=self.hop_length,
|
||||
win_length=self.win_length,
|
||||
center=not self.exact_pad,
|
||||
window=self.window.to(dtype=torch.float, device=x.device),
|
||||
return_complex=True,
|
||||
pad_mode="constant",
|
||||
)
|
||||
|
||||
def log_zero_guard_value_fn(self, x):
|
||||
if isinstance(self.log_zero_guard_value, str):
|
||||
if self.log_zero_guard_value == "tiny":
|
||||
return torch.finfo(x.dtype).tiny
|
||||
elif self.log_zero_guard_value == "eps":
|
||||
return torch.finfo(x.dtype).eps
|
||||
else:
|
||||
raise ValueError(
|
||||
f"{self} received {self.log_zero_guard_value} for the "
|
||||
f"log_zero_guard_type parameter. It must be either a "
|
||||
f"number, 'tiny', or 'eps'"
|
||||
)
|
||||
else:
|
||||
return self.log_zero_guard_value
|
||||
|
||||
def get_seq_len(self, seq_len):
|
||||
# Assuming that center is True is stft_pad_amount = 0
|
||||
pad_amount = (
|
||||
self.stft_pad_amount * 2
|
||||
if self.stft_pad_amount is not None
|
||||
else self.n_fft // 2 * 2
|
||||
)
|
||||
seq_len = torch.floor_divide(
|
||||
(seq_len + pad_amount - self.n_fft), self.hop_length
|
||||
)
|
||||
return seq_len.to(dtype=torch.long)
|
||||
|
||||
@property
|
||||
def filter_banks(self):
|
||||
return self.fb
|
||||
|
||||
def splice_frames(self, x, frame_splicing):
|
||||
"""Stacks frames together across feature dim
|
||||
|
||||
input is batch_size, feature_dim, num_frames
|
||||
output is batch_size, feature_dim*frame_splicing, num_frames
|
||||
|
||||
"""
|
||||
seq = [x]
|
||||
for n in range(1, frame_splicing):
|
||||
seq.append(torch.cat([x[:, :, :n], x[:, :, n:]], dim=2))
|
||||
return torch.cat(seq, dim=1)
|
||||
|
||||
def normalize_batch(self, x, seq_len, normalize_type):
|
||||
x_mean = None
|
||||
x_std = None
|
||||
if normalize_type == "per_feature":
|
||||
batch_size = x.shape[0]
|
||||
max_time = x.shape[2]
|
||||
|
||||
# When doing stream capture to a graph, item() is not allowed
|
||||
# because it calls cudaStreamSynchronize(). Therefore, we are
|
||||
# sacrificing some error checking when running with cuda graphs.
|
||||
# if (
|
||||
# torch.cuda.is_available()
|
||||
# and not torch.cuda.is_current_stream_capturing()
|
||||
# and torch.any(seq_len == 1).item()
|
||||
# ):
|
||||
# raise ValueError(
|
||||
# "normalize_batch with `per_feature` normalize_type "
|
||||
# "received a tensor of length 1. This will result in "
|
||||
# "torch.std() returning nan. Make sure your audio length "
|
||||
# "has enough samples for a single feature (ex. at least "
|
||||
# "`hop_length` for Mel Spectrograms)."
|
||||
# )
|
||||
time_steps = (
|
||||
torch.arange(max_time, device=x.device)
|
||||
.unsqueeze(0)
|
||||
.expand(batch_size, max_time)
|
||||
)
|
||||
valid_mask = time_steps < seq_len.unsqueeze(1)
|
||||
x_mean_numerator = torch.where(valid_mask.unsqueeze(1), x, 0.0).sum(axis=2)
|
||||
x_mean_denominator = valid_mask.sum(axis=1)
|
||||
x_mean = x_mean_numerator / x_mean_denominator.unsqueeze(1)
|
||||
|
||||
# Subtract 1 in the denominator to correct for the bias.
|
||||
x_std = torch.sqrt(
|
||||
torch.sum(
|
||||
torch.where(valid_mask.unsqueeze(1), x - x_mean.unsqueeze(2), 0.0)
|
||||
** 2,
|
||||
axis=2,
|
||||
)
|
||||
/ (x_mean_denominator.unsqueeze(1) - 1.0)
|
||||
)
|
||||
x_std = x_std.masked_fill(
|
||||
x_std.isnan(), 0.0
|
||||
) # edge case: only 1 frame in denominator
|
||||
# make sure x_std is not zero
|
||||
x_std += CONSTANT
|
||||
return (x - x_mean.unsqueeze(2)) / x_std.unsqueeze(2), x_mean, x_std
|
||||
elif normalize_type == "all_features":
|
||||
x_mean = torch.zeros(seq_len.shape, dtype=x.dtype, device=x.device)
|
||||
x_std = torch.zeros(seq_len.shape, dtype=x.dtype, device=x.device)
|
||||
for i in range(x.shape[0]):
|
||||
x_mean[i] = x[i, :, : seq_len[i].item()].mean()
|
||||
x_std[i] = x[i, :, : seq_len[i].item()].std()
|
||||
# make sure x_std is not zero
|
||||
x_std += CONSTANT
|
||||
return (x - x_mean.view(-1, 1, 1)) / x_std.view(-1, 1, 1), x_mean, x_std
|
||||
elif "fixed_mean" in normalize_type and "fixed_std" in normalize_type:
|
||||
x_mean = torch.tensor(normalize_type["fixed_mean"], device=x.device)
|
||||
x_std = torch.tensor(normalize_type["fixed_std"], device=x.device)
|
||||
return (
|
||||
(x - x_mean.view(x.shape[0], x.shape[1]).unsqueeze(2))
|
||||
/ x_std.view(x.shape[0], x.shape[1]).unsqueeze(2),
|
||||
x_mean,
|
||||
x_std,
|
||||
)
|
||||
else:
|
||||
return x, x_mean, x_std
|
||||
|
||||
@torch.compile
|
||||
def forward(self, x, seq_len, linear_spec=False):
|
||||
if x.shape[1] < self.sample_rate * self.pad_min_duration:
|
||||
pad_amount = int(self.sample_rate * self.pad_min_duration) - x.shape[1]
|
||||
if self.pad_direction == "right":
|
||||
x = F.pad(x, (0, pad_amount), value=self.pad_value)
|
||||
elif self.pad_direction == "left":
|
||||
x = F.pad(x, (pad_amount, 0), value=self.pad_value)
|
||||
elif self.pad_direction == "both":
|
||||
left_pad = pad_amount // 2
|
||||
right_pad = pad_amount - left_pad
|
||||
x = F.pad(x, (left_pad, right_pad), value=self.pad_value)
|
||||
else:
|
||||
raise ValueError(
|
||||
f"{self} received an invalid pad_direction: {self.pad_direction}. "
|
||||
f"It must be one of 'left', 'right', or 'both'."
|
||||
)
|
||||
seq_len = torch.tensor([x.shape[1]], dtype=torch.float, device=x.device)
|
||||
|
||||
seq_len_time = seq_len
|
||||
seq_len_unfixed = self.get_seq_len(seq_len)
|
||||
|
||||
# fix for seq_len = 0 for streaming; if size was 0, it is always padded
|
||||
# to 1, and normalizer fails
|
||||
seq_len = torch.where(
|
||||
seq_len == 0, torch.zeros_like(seq_len_unfixed), seq_len_unfixed
|
||||
)
|
||||
|
||||
if self.stft_pad_amount is not None:
|
||||
x = torch.nn.functional.pad(
|
||||
x.unsqueeze(1), (self.stft_pad_amount, self.stft_pad_amount), "constant"
|
||||
).squeeze(1)
|
||||
|
||||
# use dither for inference as well
|
||||
if self.dither > 0:
|
||||
x += self.dither * torch.randn(
|
||||
x.shape, dtype=x.dtype, device=x.device, generator=self.generator
|
||||
)
|
||||
|
||||
# do preemphasis
|
||||
if self.preemph is not None:
|
||||
timemask = torch.arange(x.shape[1], device=x.device).unsqueeze(
|
||||
0
|
||||
) < seq_len_time.unsqueeze(1)
|
||||
x = torch.cat(
|
||||
(x[:, 0].unsqueeze(1), x[:, 1:] - self.preemph * x[:, :-1]), dim=1
|
||||
)
|
||||
|
||||
x = x.masked_fill(~timemask, 0.0)
|
||||
|
||||
x = self.stft(x)
|
||||
|
||||
# torch stft returns complex tensor (of shape [B,N,T]); so convert to magnitude
|
||||
# guard is needed for sqrt if grads are passed through
|
||||
guard = 0 if not self.use_grads else CONSTANT
|
||||
x = torch.view_as_real(x)
|
||||
x = torch.sqrt(x.pow(2).sum(-1) + guard)
|
||||
|
||||
# get power spectrum
|
||||
if self.mag_power != 1.0:
|
||||
x = x.pow(self.mag_power)
|
||||
|
||||
# return plain spectrogram if required
|
||||
if linear_spec:
|
||||
return x, seq_len
|
||||
|
||||
# disable autocast, otherwise it might be automatically casted to fp16
|
||||
# on fp16 compatible GPUs and get NaN values for input value of 65520
|
||||
with torch.amp.autocast(x.device.type, enabled=False):
|
||||
# dot with filterbank energies
|
||||
x = torch.matmul(self.fb.to(x.dtype), x)
|
||||
|
||||
# log features if required
|
||||
if self.log:
|
||||
if self.log_zero_guard_type == "add":
|
||||
x = torch.log(x + self.log_zero_guard_value_fn(x))
|
||||
elif self.log_zero_guard_type == "clamp":
|
||||
x = torch.log(torch.clamp(x, min=self.log_zero_guard_value_fn(x)))
|
||||
else:
|
||||
raise ValueError("log_zero_guard_type was not understood")
|
||||
|
||||
# frame splicing if required
|
||||
if self.frame_splicing > 1:
|
||||
x = self.splice_frames(x, self.frame_splicing)
|
||||
|
||||
# normalize if required
|
||||
if self.normalize:
|
||||
x, _, _ = self.normalize_batch(x, seq_len, normalize_type=self.normalize)
|
||||
|
||||
# mask to zero any values beyond seq_len in batch, pad to multiple of
|
||||
# `pad_to` (for efficiency)
|
||||
max_len = x.size(-1)
|
||||
mask = torch.arange(max_len, device=x.device)
|
||||
mask = mask.repeat(x.size(0), 1) >= seq_len.unsqueeze(1)
|
||||
x = x.masked_fill(
|
||||
mask.unsqueeze(1).type(torch.bool).to(device=x.device), self.pad_value
|
||||
)
|
||||
|
||||
del mask
|
||||
pad_to = self.pad_to
|
||||
if pad_to == "max":
|
||||
x = nn.functional.pad(
|
||||
x, (0, self.max_length - x.size(-1)), value=self.pad_value
|
||||
)
|
||||
elif pad_to > 0:
|
||||
pad_amt = x.size(-1) % pad_to
|
||||
if pad_amt != 0:
|
||||
x = nn.functional.pad(x, (0, pad_to - pad_amt), value=self.pad_value)
|
||||
|
||||
return x, seq_len
|
||||
|
||||
|
||||
class CohereASRFeatureExtractor(SequenceFeatureExtractor):
|
||||
"""HF-compatible feature extractor wrapping FilterbankFeatures."""
|
||||
|
||||
model_input_names = ["input_features"]
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
feature_size=64,
|
||||
sampling_rate=16000,
|
||||
padding_value=0.0,
|
||||
max_duration=30,
|
||||
n_window_size=320,
|
||||
n_window_stride=160,
|
||||
window="hann",
|
||||
normalize="per_feature",
|
||||
n_fft=None,
|
||||
preemph=0.97,
|
||||
lowfreq=0,
|
||||
highfreq=None,
|
||||
log=True,
|
||||
log_zero_guard_type="add",
|
||||
log_zero_guard_value=2**-24,
|
||||
dither=CONSTANT,
|
||||
pad_to=16,
|
||||
frame_splicing=1,
|
||||
exact_pad=False,
|
||||
mag_power=2.0,
|
||||
nb_augmentation_prob=0.0,
|
||||
nb_max_freq=4000,
|
||||
mel_norm="slaney",
|
||||
stft_exact_pad=False,
|
||||
stft_conv=False,
|
||||
device="cpu",
|
||||
**kwargs,
|
||||
):
|
||||
super().__init__(
|
||||
feature_size=feature_size,
|
||||
sampling_rate=sampling_rate,
|
||||
padding_value=padding_value,
|
||||
**kwargs,
|
||||
)
|
||||
self.max_duration = max_duration
|
||||
self.hop_length = n_window_stride
|
||||
self._device = torch.device(device)
|
||||
self._fb_config = dict(
|
||||
sample_rate=sampling_rate,
|
||||
n_window_size=n_window_size,
|
||||
n_window_stride=n_window_stride,
|
||||
window=window,
|
||||
normalize=normalize,
|
||||
n_fft=n_fft,
|
||||
preemph=preemph,
|
||||
nfilt=feature_size,
|
||||
lowfreq=lowfreq,
|
||||
highfreq=highfreq,
|
||||
log=log,
|
||||
log_zero_guard_type=log_zero_guard_type,
|
||||
log_zero_guard_value=log_zero_guard_value,
|
||||
dither=dither,
|
||||
pad_to=pad_to,
|
||||
max_duration=max_duration,
|
||||
frame_splicing=frame_splicing,
|
||||
exact_pad=exact_pad,
|
||||
pad_value=padding_value,
|
||||
mag_power=mag_power,
|
||||
nb_augmentation_prob=nb_augmentation_prob,
|
||||
nb_max_freq=nb_max_freq,
|
||||
mel_norm=mel_norm,
|
||||
stft_exact_pad=stft_exact_pad,
|
||||
stft_conv=stft_conv,
|
||||
device=device,
|
||||
)
|
||||
self._filterbank: FilterbankFeatures | None = None
|
||||
|
||||
@property
|
||||
def filterbank(self) -> FilterbankFeatures:
|
||||
if self._filterbank is None:
|
||||
fb = FilterbankFeatures(**self._fb_config)
|
||||
fb.eval()
|
||||
self._filterbank = fb.to(self._device)
|
||||
return self._filterbank
|
||||
|
||||
def get_seq_len(self, seq_len):
|
||||
return self.filterbank.get_seq_len(seq_len)
|
||||
|
||||
def __call__(
|
||||
self,
|
||||
raw_speech,
|
||||
sampling_rate=None,
|
||||
return_tensors=None,
|
||||
**kwargs,
|
||||
) -> BatchFeature:
|
||||
if isinstance(raw_speech, np.ndarray):
|
||||
raw_speech = [raw_speech]
|
||||
|
||||
seq_len = torch.tensor([s.shape[0] for s in raw_speech])
|
||||
|
||||
max_len = max(s.shape[0] for s in raw_speech)
|
||||
padded = np.zeros((len(raw_speech), max_len), dtype=np.float32)
|
||||
for i, s in enumerate(raw_speech):
|
||||
padded[i, : s.shape[0]] = s
|
||||
|
||||
audio_tensor = torch.from_numpy(padded).to(self._device)
|
||||
seq_len = seq_len.to(self._device)
|
||||
|
||||
with torch.no_grad():
|
||||
input_features, length = self.filterbank(audio_tensor, seq_len)
|
||||
|
||||
result = BatchFeature(
|
||||
{"input_features": input_features.cpu(), "length": length.cpu()}
|
||||
)
|
||||
if return_tensors is not None:
|
||||
result = result.convert_to_tensors(return_tensors)
|
||||
return result
|
||||
|
||||
|
||||
class CohereASRProcessor(ProcessorMixin):
|
||||
"""HF-compatible processor combining CohereASRFeatureExtractor and a
|
||||
tokenizer."""
|
||||
|
||||
feature_extractor_class = "CohereASRFeatureExtractor"
|
||||
tokenizer_class = "AutoTokenizer"
|
||||
|
||||
def __init__(self, feature_extractor, tokenizer):
|
||||
super().__init__(feature_extractor, tokenizer)
|
||||
|
||||
def __call__(
|
||||
self,
|
||||
text=None,
|
||||
audio=None,
|
||||
sampling_rate=None,
|
||||
return_tensors=None,
|
||||
**kwargs,
|
||||
):
|
||||
if audio is not None:
|
||||
result = self.feature_extractor(
|
||||
audio,
|
||||
sampling_rate=sampling_rate,
|
||||
return_tensors=return_tensors,
|
||||
)
|
||||
else:
|
||||
result = BatchFeature()
|
||||
|
||||
if text is not None:
|
||||
text_inputs = self.tokenizer(text, return_tensors=return_tensors, **kwargs)
|
||||
result["input_ids"] = text_inputs["input_ids"]
|
||||
|
||||
return result
|
||||
|
||||
|
||||
AutoFeatureExtractor.register("CohereASRFeatureExtractor", CohereASRFeatureExtractor)
|
||||
AutoProcessor.register("CohereASRProcessor", CohereASRProcessor)
|
||||
@@ -910,9 +910,7 @@ class Scheduler(SchedulerInterface):
|
||||
# 2. Wrap up all the KV cache load / save ops into an opaque object
|
||||
# 3. Clear the internal states of the connector
|
||||
if self.connector is not None:
|
||||
meta: KVConnectorMetadata = self.connector.build_connector_meta(
|
||||
scheduler_output
|
||||
)
|
||||
meta = self._build_kv_connector_meta(self.connector, scheduler_output)
|
||||
scheduler_output.kv_connector_metadata = meta
|
||||
|
||||
# Build the connector meta for ECConnector
|
||||
@@ -926,6 +924,11 @@ class Scheduler(SchedulerInterface):
|
||||
self._update_after_schedule(scheduler_output)
|
||||
return scheduler_output
|
||||
|
||||
def _build_kv_connector_meta(
|
||||
self, connector: KVConnectorBase_V1, scheduler_output: SchedulerOutput
|
||||
) -> KVConnectorMetadata:
|
||||
return connector.build_connector_meta(scheduler_output)
|
||||
|
||||
def _preempt_request(self, request: Request, timestamp: float) -> None:
|
||||
"""Preempt a request and put it back to the waiting queue.
|
||||
|
||||
|
||||
@@ -392,8 +392,10 @@ class Worker(WorkerBase):
|
||||
)
|
||||
|
||||
# Profile CUDA graph memory if graphs will be captured.
|
||||
# Skip on ROCm/HIP as graph pool handles and mem_get_info behave
|
||||
# differently and can produce incorrect/negative estimates.
|
||||
cudagraph_memory_estimate = 0
|
||||
if not self.model_config.enforce_eager:
|
||||
if not self.model_config.enforce_eager and not current_platform.is_rocm():
|
||||
cudagraph_memory_estimate = self.model_runner.profile_cudagraph_memory()
|
||||
|
||||
# Use the pre-cudagraph torch peak to avoid double-counting.
|
||||
@@ -406,6 +408,8 @@ class Worker(WorkerBase):
|
||||
+ profile_result.weights_memory
|
||||
)
|
||||
|
||||
# On ROCm, cudagraph_memory_estimate is always 0 so this is a no-op.
|
||||
# On CUDA, respect the opt-in flag as originally designed.
|
||||
cudagraph_memory_estimate_applied = (
|
||||
cudagraph_memory_estimate
|
||||
if envs.VLLM_MEMORY_PROFILER_ESTIMATE_CUDAGRAPHS
|
||||
@@ -517,7 +521,6 @@ class Worker(WorkerBase):
|
||||
|
||||
def update_max_model_len(self, max_model_len: int) -> None:
|
||||
"""Update max_model_len after auto-fit to GPU memory.
|
||||
|
||||
This is called when max_model_len=-1 is used and the engine
|
||||
automatically determines the maximum context length that fits
|
||||
in GPU memory. Workers need to update their cached max_model_len
|
||||
|
||||
Reference in New Issue
Block a user