ggaaooppeenngg

为什么计算机科学是无限的但生命是有限的

FlashAttention是一种新型的注意力算法,它能够准确计算注意力,且只需进行远远少于传统方法的内存访问。这个算法的主要目标是尽可能避免内存的读取和写入,这是注意力机制性能瓶颈的一个关键因素。该论文提出了一个IO-aware的精确注意力算法,它使用tiling(贴瓷砖,代表数据分片)来减少GPU高带宽内存与低带宽内存之间的内存读取/写入次数。

该算法基于注意力矩阵通常稀疏这一观察结果:注意力矩阵只有少数元素非零。它通过将输入矩阵Q、K、V分成更小的块来实现,从而避免了计算全矩阵乘积Q*K^T的内存占用问题。通过块级别的处理,FlashAttention使得矩阵操作可以在现代GPU的内存限制下进行,并仅读取/写入每个切片的非零元素。这降低了需要的内存访问次数,使整个过程更快和更高效。

FlashAttention通过分“瓦片化”的方式计算能够更快的一个原因是将矩阵放入更高速的缓存当中,高速的叫SRAM,低速的叫HBM。

第一代 FlashAttention 只是把QK切片,这个只要把矩阵切分在SRAM,然后计算出结果再存回HBM,这个比较简单。

第二代 FlashAttention 把 softmax 的计算也放在了SRAM上。

源自博客描述的结构中可以看出。

他这里面标得感觉不是很清楚,其中的O_2应该是最终的结果O,里面的l_1/l_2 * A^1 / l_1 就还原出了最终结果的分母,也就是scale法则。

第三代 FlashAttention 减少了上面提到的scale,不再每一步做除法,而是放到最后再除。还有就是针对交叉注意力中的mask的优化,跳过了被mask的部分。还有就是CUDA Thread warps的优化提高了并行度。

阅读全文 »

vLLM 基于 uvicorn + FastAPI 的异步 Web 框架构成。vLLM 的主体是 LLMEngine,它是一个单例类,负责管理所有的模型和数据。在异步 API 中使用的是一个 AsyncEngine。在分析 AsyncEngine 之前,我们先将 Web 部分单独拆出来看一下。

vLLM 的 CLI 入口是 vllm/scripts.py,其中 serve 的启动是通过 uvloop.run 的方式启动的。uvloop 是一个替代默认 asyncio 事件循环的库,它使用 libuv 作为事件循环的实现,从而提高性能。uvicorn 是一个基于 uvloop 的 ASGI 服务器,它可以将 ASGI 应用部署到 Web 服务器上。FastAPI 是一个基于 Starlette 的 Web 框架,它提供了许多便利的功能,比如自动文档生成、请求参数校验等。

参数经过解析以后会进入 run_server,通过 uvloop.run(run_server(args))run_serverentrypoints/openai/api_server.py 下面。AsyncEngineArgs.from_cli_args(args) 使用命令行参数初始化 AsyncEngineArgs,如果要自行封装的话可以直接初始化 AsyncEngineArgsAsyncEngineArgs 继承自 EngineArgs,其中的参数都是用来控制推断命令的。

比较常用的几个参数:

  • model: 模型的路径,可以是一个目录,也可以是 hf 上的一个 repo。

  • model_name: 如果是目录的话,期望的模型名称,或者想要改个别名,对应的是 API 中指定模型的名称。

  • tensor_parallel_size: tensor parallel 副本数,如果用多个 GPU 可以用到,会根据这个将 kv head 平分到不同的 GPU 上。

  • pipeline_parallel_size: pipeline stages 数,如果用多个 GPU 可以用到,会根据这个将模型的前向计算的layers分成多个阶段,每个阶段在不同的 GPU 上计算。

    可以参考下面这个例子:

    假设我们有 8 个 GPU,分别表示为 g0 … g7,并且我们使用 2 个 GPU 来并行化模型张量,使用 4 个 GPU 来并行化模型流水线。当前函数将创建 4 个张量模型并行组和 2 个流水线模型并行组:

    4 个张量模型并行组:

    • [g0, g1]
    • [g2, g3]
    • [g4, g5]
    • [g6, g7]

    2 个流水线模型并行组:

    • [g0, g2, g4, g6]
    • [g1, g3, g5, g7]

    注意,为了提高效率,调用者应确保相邻的 rank 位于同一个 DGX 盒子上。例如,如果我们使用 2 个 DGX-1 盒子,总共有 16 个 GPU,rank 0 到 7 属于第一个盒子,rank 8 到 15 属于第二个盒子。

  • num_seqs: 最大的序列数,其实就是 batch size,会翻倍得增加显存使用,这个貌似在启动之前的 profile 阶段可能会导致大量显存的占用。

  • quantization: 量化的方法,可以是 bitsandbytes 等,可能需要和 load_format 结合使用。

  • load_format: 加载模型的格式,可以是 pt, safetensors, bitsandbytes 等等,如果用到量化的模型基本要改成 bitsandbytes。

  • dtype: 数据类型,fp32, fp16,bf16 等等,如果模型是 bf16 的话,他默认是 bf16 的模型用 bf16,有些显卡不支持 bf 浮点数所以要设置成 half 也就是 fp16。

  • host: 监听地址。

  • port: 监听端口。

  • max_model_len: 上下文长度,适合显存不足的显卡,把默认的上下文长度改下一点。

  • enforce_eager: 是否强制使用 eager 模式,如果显存不够的需要开启这个模式,不完全加载计算图的方式可以减少显存的使用。

api_server 中的 build_app 会使用 APIRouter 初始化路由,并通过 app.include_router 引入。

主要看 @router.post("/v1/chat/completions") 注册的 async def create_chat_completion 是最常用的函数调用。

init_app_stateapp.state 中保存了 openai_serving_chat,以及其他一些接口的状态,这取决于模型配置中是否包含这些功能。例如,文本嵌入等功能(通常都有)。当调用 create_chat_completion 时,会调用 openai_serving_chat 对应的 OpenAIServingChat 类的方法。因此,Serving 的主体可以通过查看这个对象的方法来理解其功能。

构建 AsyncEngine -> 构建 app 对象。

阅读全文 »

这篇文章对VLM的架构解释得非常清楚。

一种方法是使用适配器将图片转换为tokens,例如LLaVA使用的prompt based适配器。这种方法类似于RAG的形式,将图片理解的内容补充在对话的上文中。这种适配器会占用LLM的上下文长度,因为图片的tokens会被放入LLM的上文中。目前来说性能会好一些。

阅读全文 »