上一篇文章中我们介绍了 vLLM 的代码架构,我们提到 vLLM 有一个调度器模块,它负责决定每个推理阶段的任务调度。调度器主要负责选择哪些数据需要送到模型去推理,其中也实现了连续批处理的功能。在这篇文章中,我们先介绍一下调度前的准备工作,主要是输入的预处理以及把输入合并成 SequenceGroup 的过程。
在下一篇文章中,我们会介绍调度预算(SchedulingBudget)是如何影响任务的分配决策的,调度器如何将多个任务批量发送到 GPU 执行,如何处理优先级高的任务?调度器如何决定抢占低优先级任务等。
本系列的代码基于 vLLM 的 0.6.3 版本介绍
首先让我们从一个小 demo 作为切入口然后逐步深入 vLLM 调度器的源码实现。
from vllm 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='meta-llama/Llama-2-7b-hf')
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}")
在这个 demo 中,我们首先创建了一个 LLM 对象,然后定义了一些 prompts 和采样参数。接着,我们调用 llm.generate() 方法来执行推理任务,最后打印输出结果。
由此可以窥见所有的推理请求都是通过 llm.generate() ,下面我们开始剖析这个方法的内部实现。
在 vLLM 中,LLM.generate() 方法是用户与模型进行交互的核心入口。这个方法通过一系列精心设计的步骤,将用户提供的 prompts 转换为推理任务,并通过调度器有效执行这些任务。
当你调用 LLM.generate() 方法时,vLLM 会处理你提供的 prompts 和采样参数,将它们转化为可供推理执行的任务。这个方法的目标是将你的输入(无论是单个 prompt 还是多个 prompts)封装为系统可以识别的对象,然后通过智能的批处理机制来提高推理效率。
outputs = llm.generate(prompts, sampling_params)
上面这行代码简单地调用了 generate() 方法,但在内部却有一系列复杂的步骤。首先,prompts 是你提供的输入文本,sampling_params 控制模型生成文本时的行为,比如温度(temperature)和采样概率(top_p)。接下来,generate() 方法会逐步处理这些输入,首先将 prompts 转换为可以理解的格式,然后调用 LLM 引擎来执行推理任务。

picture 0
在 generate() 方法内部,第一步是处理用户提供的 prompts。prompts 的格式可能有多种:它可以是单个字符串(即一个 prompt),也可以是一个包含多个字符串的列表(多个 prompts)。此外,用户还可以选择直接传入 token ids,而不是文本。为了处理这种多样化的输入,generate() 会根据不同情况来处理输入,确保所有的输入最终都转化为统一的格式。
parsed_prompts = self._convert_v1_inputs(
prompts=cast(Optional[Union[str, List[str]]], prompts),
prompt_token_ids=prompt_token_ids,
)
在这里,_convert_v1_inputs() 方法会将 prompts 转换为 TextPrompt 或 TokensPrompt 对象。这一步的作用是保证后续的处理统一使用标准化的数据格式。 parsed_prompts 的类型是 List[PromptType]。 PromptType 可以直接理解为一个字典,它包含了 prompt 的文本、token ids 等信息。