记一个关于 RMSNorm 实现上的细节
背景在 RMSNorm 的实现中,都会强制把输入转为 float32 再进行计算,主要为了避免在计算过程中出现溢出的情况,特别是 float16。 bfloat16 由于动态范围更大,通常不会出现溢出问题,但在一些特定的计算场景下,仍然可能会遇到精度问题。因此,在实现 RMSNorm 时,强制转换为 float32 是一个通用的做法。 RMSNorm123456789101112131415import torchimport torch.nn as nnclass RMSNorm(nn.Module): def __init__(self, dim, eps=1e-8): super(RMSNorm, self).__init__() self.weight = nn.Parameter(torch.ones(dim)) self.eps = eps def forward(self, x): # 强制转换为 float32 x = x.to(torch.float32) # 计算 ...
Qwen2.5-VL 中视觉模型的窗口注意力机制
背景在算能的 LLM-TPU 里有对 Qwen2.5-VL 视觉模型的导出,但是并没有增加对窗口注意力的支持,因此跟 Qwen2.5-VL 原生相比,性能下降较大。 UPDATE: 算能官方已经支持 qwen2.5-vl 的窗口注意力,具体实现参考 tpu-mlir/llm。 qwen2.5-vl 视觉编码器的注意力机制qwen2.5-vl 中有两种不同的注意力机制: 全局注意力:用于处理全图特征,但是计算量较大,计算量增长随着图像尺寸的增加而呈平方增长,因此只在 4 个特殊的 layer 使用。 窗口注意力:只计算局部区域的注意力,计算量较小,适用于大多数层。 qwen2.5-vl 的视觉编码器注意力计算如下: 1234567891011121314151617181920212223class Qwen2_5_VLVisionFlashAttention2(nn.Module): ... def forward( self, hidden_states: torch.Tensor, cu_seqlens: t...
论文总结 | Next Token Prediction Towards Multimodal Intelligence A Comprehensive Survey
背景文章介绍了截至 2024 年,多模态大模型的进展。文章从词元化(tokenization)、骨干网络(backbone)、训练方式(training)、数据集(dataset)四个方向进行总结,并给出了多模态模型的未来发展方向及挑战。这对于我们了解和部署多模态大模型非常有帮助。 同时,本文主要是梳理相关基础内容,暂不对论文中给出的所有参考文献做详细解析,仅对关键点进行总结。 模型流水线组成 这张图主要分为三个部分: Tokenization是将输入数据转换为模型可以处理的形式的过程。图中展示了两种类型的词元化: Discrete Tokens:输入图像通过Encoder转换为特征表示,这些特征表示通过Vector Quantization过程转换为离散的词元(如数字7、2、9等)。为了确保词元的生成质量,离散词元在训练阶段还需要通过Decoder转换回图像。 Continuous Tokens:输入图像同样通过编码器转换为特征表示。这些特征表示通过Projector转换为连续的词元。 Modeling部分展示了Transformer模型的内部结构及其工作原理: De...
Self-Attention 算法简介
MHA MQA GQA 对比MHA Multi-Head Attention,MQA Multi-Query Attention,GQA Group-Query Attention。在 transformers 中,主要体现在 config.json 里的 num_key_value_heads 设置上。 QKV shape 如下: Query: $[B, L, D_{\text{model}}]$ -> $[B, L, N_\text{heads}, D_\text{head}]$ -> $[B, N_\text{heads}, L, D_\text{heads}]$ Key & Value: $[B, L, D_\text{kv}]$ -> $[B, N_\text{kv}, L, D_\text{head}]$ 其中,$B$ 表示 batch size,$L$ 表示 sequence length,$D_\text{model}$ 为 hidden size,$N_\text{heads}$ 为 num heads,即多头注意力里的头数。 re...
ONNX 静态图转换为动态图
模型转换ONNX 模型从动态图变为静态图是非常容易的,使用 onnxsim --overwrite-input-shape input_name:dim0,dim1,dim2 model.onnx 即可,onnxsim 内部会将动态维度转为静态维度,同时执行图优化。 但是如果只有静态图,想要变回动态图,则比较困难。一般来说,Batch 维度是最容易改变的,修改 Batch 维度可以一次推理 N 个输入,提高吞吐量。 使用 onnx2torch 可以解决这个问题。首先将 ONNX 模型转为 pytorch 模型,然后重新使用 torch.onnx.export 导出一个有动态维度的模型即可。代码如下: 1234567891011121314151617181920import onnximport torchfrom onnx2torch import convert# onnx -> torchonnx_model_path = "/some/path/mobile_net_v2.onnx"torch_model_1 = convert(onnx_mod...
CPU 忙等的死锁案例
一个死锁案例代码部分逻辑: 1234567for (int i = tid; i < work_size; i += thread_num) { while (is_work_ready[i] == 0) { // spin lock } // do work foo(work[i]);} 下面这段代码看起来是合理的,线程执行的状态依赖于 is_work_ready[i],通过这个变量实现主线程和子线程之间通信。但是实际上由于 CPU 会陷入忙等(busy-waiting),导致当主线程更新 flag 之后,子线程不会读取这个变量,陷入死锁。即「如果 is_work_ready 为 1,正常执行」,但是「is_work_ready 为 0 时,陷入死锁,无法恢复」。 有关多线程编程的方式: 如果代码涉及到主线程与子线程的通信,最好使用同步 API,不要使用简单的 dead loop 。比如 ChatGPT 建议使用 std::mutex + std::conditional_variable 通信。 dead lo...
MiniCPM-O-2_6 模型结构介绍
目的介绍一下有关 VLM/MLLM 的模型结构,从模型结构上指导模型部署,了解各个模块的作用。 MiniCPM-O-2_6 简介O 代表了 Omni,含义是全能。MiniCPM-O-2_6 是一个基于 MiniCPM-2 的多模态模型,在之前的 MiniCPM-V-2_6 的基础上增加了额外的多模态能力,包括【语音识别】、【语音生成】的功能。由于语音作为文本嵌入到了 LLM 的输入之中,还可以实现【语音提问图片内容,语音回复】的功能。OpenBMB 官方博客见 MiniCPM-o 2.6: A GPT-4o Level MLLM for Vision, Speech, and Multimodal Live Streaming on Your Phone。 为了实现上面的功能,MiniCPM-O-2_6 一共有四个模块: SigLip-400M 视觉编码器:用于提取视觉特征,并映射到 LLM 嵌入空间 Whisper-medium-300M 语音编码器:用于提取语音特征,也映射到 LLM 嵌入空间 LLM:基于 Qwen2.5-7B 模型,能够支持多模态输入,但是受限...
关于 DeepSeek-R1-Distill-Qwen-1.5B 显存占用比下载权重高非常多的现象分析
现象从 huggingface 上下载下来的 DeepSeek-R1-Distill-Qwen-1.5B 模型权重大小只有 3.5GB,但是加载到 GPU 上之后,占用显存达到 5.4GB,多了 1.9GB。 排查显存占用比权重多 1.9GB 的原因应该是模型在创建的时候申请了权重以外的内存,这部分内存没有体现在权重的大小中。 使用 pytorch 查看显存占用pytorch 博客 Understanding GPU Memory 1: Visualizing All Allocations over Time 中给出了 pytorch 内部的显存统计方法,函数都定义在 torch.cuda.memory 中。具体代码可以参考官方博客,下面展示显存统计结果。 可以看到从程序开始到结束,显存一直都是 5.4GB 左右,最上方有一小部分激活占用的显存,但是由于我们输入非常短,所以占用很小。从这个结果我们可以想象到,模型总显存占用比权重多的那部分显存在模型一开始就创建了,后续的应用层是无法操作的。 使用 transformers 查看显存占用transformers 的 AutoMo...
opencl-tips
为什么从 CPU 上拷贝到 Image 里使用的是【CPU -> Buffer -> Image】而不是【CPU -> Image】?因为 Image 一般都是用 RGBA 格式,需要填充为 4 通道,以及长宽 4 对齐等需求。如果在 CPU 上做,需要对内存进行补边再拷贝到 Image 里;不如【CPU -> Buffer -> Image】 的兼容性更好。前者不用考虑 clEnqueueWriteBuffer 的参数,在 buffer_to_image 内核中注意边界条件即可。 为什么大部分 OpenCL 编程使用的都是全局内存,少用局部内存/共享内存?【不使用局部内存】可能是一种编程规范。这样做有以下优点: 可以通过在线调优 local_work_size 的方式找到最优值,从而提高性能,因为使用局部内存可能需要已知的 lws 大小;如果不需要局部内存,lws 的值可以是任意的,从而可以更灵活地调优。 不同厂商(vendor)的实现可能不同,使用全局内存可以避免不同厂商的差异。比如 mali GPU 使用的是 global memo...
将 ViT 的第一个卷积层转为 MatMul,要求步长和卷积核相等
ViT 卷积层为了将输入的图片转为 patch,然后送入到 Transformer 中,ViT 第一个卷积的步长和卷积核大小是相等的,都是 16。如下图所示, 某些框架可能不支持这么大的 Conv 步长,导致无法转换相应模型。 MatMul卷积可以使用 im2col 加 matmul 来实现,但是由于 im2col 可能效率不高,所以一般不使用这个方式。 不过我们注意到 ViT 第一个卷积层里的步长和卷积核大小是相等的,所以我们可以用 reshape + transpose 来实现 im2col。代码如下: 1234567891011121314151617181920212223242526def conv2d_to_matmul(inputs, kernel, stride, padding, bias): # 保证 stride 和 kernel 的 size 相等 assert stride == kernel.shape[2] assert stride == kernel.shape[3] assert padding == 0 out...