# mytransformer **Repository Path**: freechatgpt/mytransformer ## Basic Information - **Project Name**: mytransformer - **Description**: 手搓Qwen2-VL推理框架 - **Primary Language**: Unknown - **License**: Not specified - **Default Branch**: main - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 0 - **Forks**: 0 - **Created**: 2026-07-13 - **Last Updated**: 2026-07-17 ## Categories & Tags **Categories**: Uncategorized **Tags**: None ## README # mytransformer —— 从零用 PyTorch 实现 Qwen2-VL-2B 推理 本项目不依赖 `transformers` 的模型前向,仅用它做**分词**与**图像预处理**, 真正的 Transformer 推理流程(视觉编码器 + 语言模型 + 自回归生成)全部用纯 PyTorch 手写实现。 模型权重:`/workspace/Qwen2-VL-2B-Instruct`(HuggingFace 格式,bf16,约 4.4GB)。 --- ## 1. 运行方式 ```bash cd /workspace/mytransformer /workspace/.venv/bin/python -u main.py ``` > 当前环境复用已装好 `torch` / `transformers` 的 `/workspace/.venv`。 > 若改用 `uv sync` 新建环境,需联网拉取 `torch`(约 2GB)。 运行结果(GPU bf16,约 4.6GB 显存): ``` [文本] 我是来自阿里云的大规模语言模型,我叫通义千问。 (2.2s) [图片] 这张图片展示了一位年轻女子和她的拉布拉多犬在海滩上玩耍的场景。 女子穿着格子衬衫和黑色裤子,坐在沙滩上,她的手伸向狗,狗抬起前爪, 似乎在进行握手或击掌的动作。背景是晴朗的天空和远处的海浪... (4.8s) ``` 输出与 `transformers` 官方版**逐字一致**,证明从零实现正确。 --- ## 2. 项目结构 | 文件 | 说明 | |------|------| | `model.py` | 手写全部推理:视觉编码器 ViT、语言模型 Qwen2、M-RoPE、KV Cache 生成 | | `main.py` | 运行入口:用 `AutoProcessor` 做预处理,调用手写模型生成 | | `pyproject.toml` | uv 项目依赖声明 | --- ## 3. 架构设计 Qwen2-VL 采用**「视觉编码器 + 语言解码器」融合**架构:视觉塔(ViT)把图片/视频编码成视觉 token 序列, 语言塔(Qwen2 Decoder)把文本 token 与视觉 token 拼接后自回归生成。两者通过 **M-RoPE 三维位置编码** 和 **image_token 占位符替换** 两个机制对齐到同一语义空间。 ### 3.1 整体架构与端到端数据流 详细解读https://chatgpt.com/share/6a59f6c6-ab74-83e8-adb9-b7ff12dc5cb9 ``` ┌───────────────────────────────────────────────────────────────────────┐ │ 输入 │ │ 文本 ──分词器──▶ input_ids [1, seq] (含 image_token_id=151655 占位)│ │ 图片 ──处理器──▶ pixel_values [N, 1176] │ │ image_grid_thw [[t, h, w]] (空间网格尺寸) │ └───────────────┬───────────────────────────────┬───────────────────────┘ │ │ ▼ ▼ ┌──────────────────────────┐ ┌────────────────────────────────────┐ │ 视觉编码器 VisionModel │ │ 语言模型 Qwen2VL │ │ 32 层 ViT │ │ · 词嵌入 (与 lm_head 共享权重) │ │ Conv3d→线性映射 patch │ │ · 28 层 Decoder (Pre-LN) │ │ 2D-RoPE │ │ · M-RoPE (t/h/w 三维位置) │ │ PatchMerger │ │ · GQA + SwiGLU + KV Cache │ │ └─▶ 视觉特征 [V, 1536] │ │ │ └────────────┬──────────────┘ └─────────────────┬──────────────────┘ │ V = t·(h//2)·(w//2) 个视觉 token │ │ │ └──────────────┬─────────────────────────┘ ▼ ┌──────────────────────────────────────────────┐ │ 视觉-语言桥接:把 input_ids 中的 image_token │ │ 占位符替换成视觉特征,得到 input_embeds │ │ input_embeds = [1, seq, 1536] │ └──────────────────────┬───────────────────────┘ ▼ ┌──────────────────────────────────────────────┐ │ M-RoPE 位置索引计算 get_rope_index │ │ → position_ids = [3, seq] │ └──────────────────────┬───────────────────────┘ ▼ ┌──────────────────────────────────────────────┐ │ 自回归生成 (KV Cache) │ │ prefill 阶段:整段输入并行前向 │ │ decode 阶段:每步只算新 token,拼接历史 KV │ └──────────────────────┬───────────────────────┘ ▼ 输出文本 token 序列 ``` **端到端步骤(`Qwen2VL.generate`):** 1. 文本经分词得到 `input_ids`,其中图片位置被替换为 `image_token_id`(占位符,数量 = 视觉 token 数)。 2. 图片经处理器得到 `pixel_values` 与 `image_grid_thw`。 3. 视觉编码器前向,得到视觉特征序列 `[V, 1536]`。 4. 词嵌入查表得到文本 `input_embeds`,将 `image_token` 对应位置的向量替换为视觉特征。 5. `get_rope_index` 计算三维位置 `position_ids [3, seq]`。 6. 语言模型 prefill(带因果掩码)→ 输出 logits → 取末位 token 作为首生成。 7. 进入 decode 循环:每步把新 token 喂入、拼接 KV Cache、采样下一个 token,直到 `eos` 或达到最大长度。 ### 3.2 视觉编码器(VisionModel)结构 视觉塔是一个标准 ViT(32 层),但 patch 化与位置编码针对视频/图像的 3D 网格做了定制: ``` pixel_values [N, 1176] (N = t·h·w,例如单图 1·36·54 = 1944) │ ▼ Patch Embedding:等价于对每个 3D patch 做 [1280, 1176] 线性映射 hidden [N, 1280] │ ├─────────────────────────────────────────────────────┐ │ 重复 32 次 (Pre-LN Transformer Block): │ │ x ──LayerNorm──▶ qkv 投影 ──2D-RoPE──▶ 注意力 ──▶ │ │ └─────────────────────────────────┐ │ │ x ──LayerNorm──▶ MLP(quick_gelu) ───────┘ │ │ residual 连接 │ └─────────────────────────────────────────────────────┘ hidden [N, 1280] │ ▼ PatchMerger │ · LayerNorm │ · 将 2×2 相邻 patch 拼接 → 1280×4 = 5120 │ · GELU MLP: 5120 → 5120 → 1536 ▼ 视觉特征 [V, 1536] (V = t·(h//2)·(w//2),例如单图 1·18·27 = 486) ``` 要点: - **Patch Embedding 的 Conv3d 等价性**:官方权重 `visual.patch_embed.proj` 是 Conv3d `[1280, 3, 2, 14, 14]`, 由于卷积核恰好覆盖整个 3D patch 且 stride = kernel,**等价于对每个 patch 做 `[1280, 1176]` 线性映射** (`1176 = 3×2×14×14`),因此实现为 `F.linear(x, w.reshape(1280, 1176))`。 - **2D-RoPE**:按 `spatial_merge_size=2` 重排的 h/w 网格生成位置,频率维 `dim = head_dim//2 = 40`。 - **分块注意力掩码**:多图之间用块对角掩码隔离(单图即全连接),掩码显式用 `float32` 避免 bf16 下 `-inf` 异常。 ### 3.3 语言模型(LanguageModel / Qwen2 Decoder)结构 语言塔是 28 层 Pre-LN Decoder,采用 GQA + SwiGLU,并使用 M-RoPE 替代普通 RoPE: ``` input_embeds [1, seq, 1536] (image_token 位置已被视觉特征替换) │ ├─────────────────────────────────────────────────────┐ │ 重复 28 次 (DecoderLayer): │ │ x ──RMSNorm──▶ 自注意力 ──────────────▶ + residual│ │ (M-RoPE → GQA → SDPA → o_proj) │ │ x ──RMSNorm──▶ SwiGLU MLP ─────────▶ + residual │ │ (down(silu(gate(x)) · up(x))) │ └─────────────────────────────────────────────────────┘ │ ▼ RMSNorm ▼ lm_head (与 embed_tokens 共享权重) logits [1, seq, 151936] ``` - **RMSNorm**:`x / sqrt(mean(x²) + eps) · weight`,在 float32 下计算以稳定数值。 - **GQA**:`num_heads=12`,`num_kv_heads=2`,KV 头沿 head 维 `repeat_interleave(6)` 扩展到 12 个。 - **KV Cache**:prefill 阶段 `is_causal=True` 一次算完全部;decode 阶段只算新 token 并拼接历史 KV,避免重复计算。 - **权重共享**:词嵌入矩阵与 `lm_head` 使用同一份 `model.embed_tokens.weight`。 ### 3.4 视觉-语言桥接:占位符替换机制 `input_ids` 中图片位置被填为 `image_token_id = 151655`(一个特殊占位符),其**数量恰好等于视觉 token 数 V** (由 `image_grid_thw` 推算:`V = t·(h//2)·(w//2)`)。生成 `input_embeds` 时: ``` text_embeds = embed_tokens(input_ids) # [1, seq, 1536] mask = (input_ids == image_token_id) # 标记占位位置 text_embeds[mask] = visual_features # 把占位符换成视觉特征 ``` 这样就无需任何 cross-attention,视觉与文本 token 在同一序列里用统一的自注意力交互。 ### 3.5 M-RoPE:多模态旋转位置编码机制 普通 RoPE 每个位置只有一个标量;Qwen2-VL 的 M-RoPE 把位置扩展为 **(t, h, w) 三维**,分别刻画 **时间 / 高度 / 宽度**,再按维度切分给不同的 head 子空间: ``` head_dim = 128,按 mrope_section = [16, 24, 24] 切分: ┌──────────┬──────────┬──────────┬──────────┬──────────┬──────────┐ │ 0..16 │ 16..40 │ 40..64 │ 64..80 │ 80..104 │ 104..128 │ │ t 维 │ h 维 │ w 维 │ t 维 │ h 维 │ w 维 │ │ (时间) │ (高度) │ (宽度) │ (时间) │ (高度) │ (宽度) │ └──────────┴──────────┴──────────┴──────────┴──────────┴──────────┘ 前半段 64 维 后半段 64 维(结构相同,重复拆分) ``` - **文本段**:t/h/w 三个维度位置相同(退化为 1D RoPE)。 - **图像段**:t 恒为 0(单图),h/w 取合并后网格坐标;段内位置从 `current_pos` 起算。 - **关键偏移**:图像/视频段结束后,`current_pos += max(h, w) // spatial_merge_size` (即合并后网格的空间跨度),而**不是**累加视觉 token 数——这是初版实现出错的地方(见第 5 节问题 2)。 ### 3.6 张量形状流转表 | 阶段 | 张量 | 形状(单图示例) | 说明 | |------|------|------------------|------| | 输入 | `input_ids` | `[1, seq]` | 含 `151655` 占位符 | | 输入 | `pixel_values` | `[1944, 1176]` | `N = t·h·w = 1·36·54` | | 输入 | `image_grid_thw` | `[[1, 36, 54]]` | 原始网格(未合并) | | 视觉 | patch embed 输出 | `[1944, 1280]` | 线性映射 | | 视觉 | 32 层后 | `[1944, 1280]` | ViT 特征 | | 视觉 | merger 输出 | `[486, 1536]` | `V = 1·18·27` | | 桥接 | `input_embeds` | `[1, seq, 1536]` | 占位符被视觉特征替换 | | 位置 | `position_ids` (M-RoPE) | `[3, seq]` | t/h/w 三维 | | 语言 | Q/K/V | Q:`[1,seq,12,128]` K/V:`[1,seq,2,128]` | GQA,KV 头=2 | | 语言 | 注意力后(GQA 展开) | K/V:`[1,seq,12,128]` | `repeat_interleave(6)` | | 输出 | `logits` | `[1, seq, 151936]` | 取末位用于 decode | --- ## 4. 核心模块实现细节 ### 4.1 视觉编码器(ViT) - **Patch Embedding**:权重 `visual.patch_embed.proj` 是 Conv3d `[1280,3,2,14,14]`。 官方把 `pixel_values` 直接 `view(-1, 3, 2, 14, 14)` 喂给 Conv3d。 由于卷积核 = 整个 3D patch 且 stride = kernel,**等价于对每个 patch 做 `[1280, 1176]` 的线性映射**, 因此实现为 `F.linear(x, w.reshape(1280, 1176))`,`x` 形状 `[N, 1176]`。 - **2D 旋转位置编码**:按 `spatial_merge_size=2` 重排的 h/w 网格生成位置, 用 `VisionRotaryEmbedding`(dim = head_dim//2 = 40)构造 cos/sin。 - **32 层 Transformer**:`LayerNorm → Attention(含 RoPE) → MLP(quick_gelu)`。 多图之间用分块全连接掩码隔离(单图即全连接)。 - **PatchMerger**:`LayerNorm → 4 个相邻 patch 拼接(1280×4=5120) → GELU MLP → 1536`。 输出 token 数 = `t × (h//2) × (w//2)`(例如单图 `1×18×27 = 486`)。 ### 4.2 语言模型(Qwen2 Decoder,28 层) - **RMSNorm**:`x / sqrt(mean(x²)+eps) * weight`。 - **M-RoPE(多模态旋转位置编码)**:位置是 `[3, seq]` 的三维张量(文本 1D / 图像 2D / 视频 3D)。 把 `head_dim=128` 按 `mrope_section=[16,24,24]` 拆成三段,分别用 t/h/w 三个维度的位置, 前半段与后半段结构相同。 - **GQA(分组查询注意力)**:`num_heads=12`,`num_kv_heads=2`,KV 头沿 head 维 `repeat_interleave(6)`。 - **SwiGLU 前馈**:`down(silu(gate(x)) * up(x))`。 - **KV Cache 增量解码**:prefill 阶段 `is_causal=True`,解码阶段只算新 token 并拼接历史 KV。 - 词嵌入与 `lm_head` 权重共享(`model.embed_tokens.weight`)。 --- ## 5. 实现过程中遇到的问题与解决 ### 问题 1:模型加载到 CPU + float32 导致 OOM 被杀 - **现象**:首次后台运行时日志显示 `[model] 加载权重到 cpu (torch.float32)`,随后进程被 OOM 杀掉。 - **原因**:后台 `setsid` 启动的进程在当时 `torch.cuda.is_available()` 为 False(环境/会话状态异常), 触发了 `dtype = float32` 分支,4.4GB 权重以 float32 加载(约 8.8GB)直接爆内存。 - **解决**:确认 GPU 可用(15GB 空闲)后重新运行,强制走 `cuda + bfloat16` 分支, 显存占用降到约 4.6GB,推理正常。 ### 问题 2:M-RoPE 位置索引算错(最关键) - **现象**:图文问答输出语义混乱/不连贯。 - **原因**:初版 `get_rope_index` 对图像段后的位置偏移处理错误—— 错误地用「图像 token 数」累加位置,而官方逻辑是 `current_pos += max(h, w) // spatial_merge_size` (即合并后网格的空间跨度),且图像内部 t/h/w 三维各自加 `start_position` 偏移。 - **解决**:严格对齐 `transformers` 源码的 `get_rope_index` / `get_vision_position_ids`: 1. 按模态(文本=0 / 图片=1 / 视频=2)对 `input_ids` 连续分段; 2. 文本段:`arange(text_len)` 三维度相同,累加 `text_len`; 3. 图像/视频段:调用 `get_vision_position_ids`,t/h/w 分别构造后累加 `max(h,w)//m`。 - **验证**:修正后图文输出与官方版逐字一致。 ### 问题 3:视觉注意力掩码 dtype 异常 - **现象**:bf16 下的 `-inf` 掩码在 `scaled_dot_product_attention` 中可能数值异常。 - **解决**:把分块注意力掩码显式声明为 `torch.float32`(`[1, seq, seq]`),与官方一致。 ### 问题 4:`pixel_values` 形状的确认 - **不确定点**:Conv3d 的输入排布是否等价于线性映射。 - **验证**:用 `AutoProcessor` 实际跑一张图,确认 `pixel_values.shape = (1944, 1176)`、`image_grid_thw = [[1, 36, 54]]`、 输入中 image_token 数 = `486 = 1×(36//2)×(54//2)`, 与「线性映射 + merger 降维」的假设完全吻合,确认视觉实现正确。 ### 问题 5:脚本文件名与标准库冲突 - **现象**:临时验证脚本命名为 `inspect.py`,导入 `transformers` 时报 `ImportError: cannot import name 'AutoProcessor' from partially initialized module 'transformers'`。 - **原因**:`inspect.py` 遮蔽了 Python 标准库 `inspect`,而 `transformers` 内部 `import inspect`。 - **解决**:改名为 `inspect_in.py`,验证完成后删除。 --- ## 6. 关键配置(来自 `config.json`) | 模块 | 参数 | |------|------| | 语言模型 | hidden=1536, layers=28, heads=12, kv_heads=2, rope_theta=1e6, mrope=[16,24,24] | | 视觉编码器 | embed=1280, depth=32, heads=16, patch=14, temporal_patch=2, spatial_merge=2 | | 分词器 | vocab=151936, BPE, image_token=151655, eos=151645 | --- ## 7. 后续可扩展 - 支持**视频 / 多图**(`get_rope_index` 已预留 `video_grid_thw` 分支); - 交互式命令行对话; - 接入 `flash_attention_2` 进一步加速; - 自动降级 dtype(检测不支持 bf16 的硬件时回退 fp16/fp32,兼容 AMD ROCm 等环境)。