1. 如果我需要输出特征图,或者获取一个model中命名规律的几个层e.g. l_1 l_2的输入输出,应该怎么做?

最常用:forward hook 抓中间层输入/输出

import torch

acts = {}

def make_hook(name):
    def hook(mod, inp, out):
        # inp 是 tuple,通常取 inp[0]
        acts[name] = {
            "in":  inp[0].detach().cpu(),
            "out": out.detach().cpu() if torch.is_tensor(out) else out
        }
    return hook

handles = []
for name, m in model.named_modules():
    if name in {"l_1", "l_2"}:   # 或者 name.startswith("l_")
        handles.append(m.register_forward_hook(make_hook(name)))

model.eval()
with torch.no_grad():
    _ = model(x)   # 跑一遍 forward,acts 里就有了

# 用完记得移除 hook,避免内存泄露/重复注册
for h in handles:
    h.remove()

# 例如拿 l_1 的输出特征图
feat = acts["l_1"]["out"]   # [N,C,H,W] or [N,T,C] 等

把hook 注册进模型的每一层 nn.modules(),使得前向时可以抓取到每一层的输入和输出。在 eager 下先用 hook 抓,确认对了再去 trace/script。真要 TorchScript 下也抓:更倾向方案 2(显式返回),因为 hook 在部署环境里不好维护。

detach() 防止把计算图挂住导致显存涨
.cpu() 方便保存/可视化
handle.remove() 必须做


  1. 相对于CNN,RNN,transformer的优势在哪里?

相对于 CNN 和 RNN,Transformer 的主要优势有三点:

并行性更好(对比 RNN)

建模长程依赖更直接
Self-Attention 可以直接让任意两个位置交互;
RNN 依赖多步传播,容易梯度衰减;
CNN 需要很深或很大的 kernel 才能看到远距离。

结构统一、可扩展性强
同一套 attention 结构可以用在:

代价是 显存和计算开销大,所以在 infra 里才会有 PagedAttention、FlashAttention、KV cache 这些优化。


  1. 为什么要用layernorm,有什么用?

LayerNorm 的核心作用是:稳定训练。具体来说有三点:

CNN 里常用 BatchNorm,Transformer / RNN 里几乎都用 LayerNorm。对于 NLP 中常用的 LayerNorm,是在 [Batch Size, Sequence Length, Hidden Dim] 中的 Hidden Dim 维度进行归一化的,即是在特征维度进行归一化的。避免 latent vector 中某些维度数值过大或过小,从而影响后续层的数值稳定性和梯度传播。

❌:LN 不是 顺着句子长度(Sequence Length)把所有词揉在一起算均值;而是针对每一个词(Token),把它自己的特征向量(Hidden Dim)“抹平”。

Layer Norm 公式:

Image

方法 场景 输入 Tensor 形状 归一化计算涉及的轴 (被求和的轴) 统计量数量 物理含义
LayerNorm NLP $[B, S, H]$ $H$ (Hidden) $B \times S$ 组 无论句子多长、Batch 多大,每个词自己管自己缩放。
BatchNorm CV $[B, C, H, W]$ $B, H, W$ $C$ 组 整个批次所有图片在同一个通道上共享同一套均值标准差。

Image

Image


  1. norm有哪几种?

常见的 Normalization 主要有这几类:

**Batch Normalization(BN) **

**Layer Normalization(LN) **

Image

**Instance Normalization(IN) **

**Group Normalization(GN) **

**RMSNorm(常见于大模型) **

Image

CNN 更多用 BN / GN,
Transformer 更多用 LN / RMSNorm。


  1. 什么是attention层,QKV分别是什么,为什么这么计算,为什么要除以根号d,不除可不可以?

Attention 用来让每个 token 根据相关性,动态地聚合其他 token 的信息。

Q(Query):我在找什么信息
K(Key):我拥有什么信息标签
V(Value):真正要被聚合的内容

为什么要除以 $\sqrt{d}$ ?

防止 SoftMax 饱和,稳定训练。因为指数函数对差值很敏感,一旦某个 $z_i$ 比其他几个大几倍,那么 $s^{z_i}$ 会指数级放大,甚至直接变为 one-hot。


  1. 什么是 KV Cache?为什么需要 KV Cache?

KV Cache 是在自回归生成时,缓存每一层 Attention 里已经算过的 Key 和 Value。将历史序列中计算过的 K 和 V 保存在显存中,这样新的 Token 只需要算所以的 Q 和当前的 K 和 V 即可。复杂度从 $O(t^2) 变为了 O(t)$ 。

这里 (1, hidden dim) * (hidden dim, hidden dim) 后,每个 K/V 的 Cache 大小是 (1, hidden dim)。


  1. pd分离中,pd复杂度分别是多少,为什么p是计算密集而d是访存密集?

Prefill (P): $O(N^2)$ (Attention部分),计算密集型 (Compute-bound)。
Decode (D): $O(N)$ (单步),访存密集型 (Memory-bound)。

Prefill 阶段 (首 token 延迟 / 预处理)在这一步,模型一次性接收 $N$ 个 Token,并行计算它们的 KV Cache。线性变换 (Projection / FFN): 输入矩阵是 $[N, D]$,权重是 $[D, D]$。矩阵乘法复杂度为 $O(N \cdot D^2)$ 。Attention 计算:$Q \cdot K^T$: $[N, D] \times [D, N] \rightarrow [N, N]$。复杂度为 $O(N^2 \cdot D)$ 。Score $\cdot V$ : $[N, N] \times [N, D] \rightarrow [N, D]$ 。复杂度为 $O(N^2 \cdot D)$ 。结论: 当 Context 很长时,Attention 的 $O(N^2)$ 占主导;当 Context 较短时,线性层的 $O(D^2)$ 占主导。但无论如何,因为 $N$ 通常较大(几百到几千),总计算量非常大。

Decode 阶段 (逐 token 生成)在这一步,输入只有 1 个 Token (当前生成的),但需要和过去所有的 $N$ 个 KV Cache 进行交互。线性变换 (Projection / FFN): 输入是 $[1, D]$ ,权重 $[D, D]$ 。复杂度为 $O(D^2)$ 。Attention 计算:$Q_{new} \cdot K_{cache}^T$: $[1, D] \times [D, N] \rightarrow [1, N]$ 。复杂度为 $O(N \cdot D)$ 。Score $\cdot V_{cache}$: $[1, N] \times [N, D] \rightarrow [1, D]$ 。复杂度为 $O(N \cdot D)$ 。结论: 每生成一个 Token,复杂度随上下文长度 $N$ 线性增长,即 $O(N)$ 。

PD Separate 和 Chunked Prefill:核心区别:架构 vs. 调度

本质: 是 架构级(Architecture/Deployment) 的策略。
做法: 也就是通常说的 Splitwise。把集群里的 GPU 分成两拨,一拨专门做 Prefill(P节点),一拨专门做 Decode(D节点)。
流程: 请求先去 P 节点算完 Prompt,生成 KV Cache,然后通过网络(RDMA)把 KV Cache 传给 D 节点,D 节点接着生成后续 Token。
解决的问题: 彻底隔离计算密集型和访存密集型任务,让 P 节点可以用高算力卡(如 H800),D 节点可以用高带宽卡(甚至量化后用推理卡),实现硬件层面的资源解耦。

本质: 是 调度级(Scheduling/Batching) 的策略。
做法: 也就是 Sarathi 或 Orca 中的某些策略。它不一定需要分拆机器。
流程: 当一个很长的 Prompt 进来时,不要一次性占满 GPU 算完(因为这会阻塞住其他正在排队等着 Decode 的短请求,即 Head-of-Line Blocking)。而是把这个 Long Prompt 切成很多个小 Chunk(比如 512 token 一块)。在每一个调度步(Step)里,GPU 既处理一部分 Decode 请求,也“顺便”处理这 512 个 Prefill token。
解决的问题: 平滑延迟(Latency Smoothing)。防止一个大任务进来把 GPU 显存或计算单元锁死几百毫秒,导致其他并发用户的 Inter-Token Latency 骤增。


  1. GPU的内存hierarchy是什么?

对于 Nvidia 卡来说:

Register、 Shared Memory/ L1 Cache、 L2 Cache、 Global Memory(HBM/ DRAM)

其中,


  1. 什么是 FlashAttention?

FlashAttention 是一种高效实现 Attention 的 CUDA 算法,核心目标是:减少显存访问。

主要是将中间计算结果不写回显存,在片上完成计算。在 Online-Softmax 的基础上,将 3-Pass 通过引入调整因子实现成为 1-Pass。减少了 $QK^T$ 和 $Softmax$ 两个中间结果的传输。避免了 $S = QK^T$ ($N \times N$) 和 $P = \text{Softmax}(S)$ ($N \times N$) 这两个巨大的 $O(N^2)$ 中间矩阵写入显存。

标准 Attention 通常需要三个主要的 Kernel(或者说显存读写阶段):$Q \times K^T \rightarrow S$ (Write HBM)$\text{Softmax}(S) \rightarrow P$ (Read $S$, Write $P$)$P \times V \rightarrow O$ (Read $P, V$ , Write $O$)FlashAttention 将这三步融合进一个 Kernel。数据一旦从 HBM 读进 SRAM,就在 SRAM 里“吃干抹净”,直到算出最终的 $O$ 的一部分才写回 HBM。

具体来说,Tiling(分块)+ Recomputation(重计算)+ Kernel Fusion(算子融合)。

深度解析 Online Softmax 的魔法在分块计算中,问题在于:Softmax 需要全局的分母 $\sum e^{x_i}$ 。如果你把 $K$ 和 $V$ 切成了两块(Block 1 和 Block 2):算 Block 1 时,你只知道 Block 1 的最大值 $m_1$ 和指数和 $\ell_1$ 。算 Block 2 时,你发现了更大的最大值 $m_2$ 。FlashAttention 的核心 Trick:它维护了两个运行时的统计量: $m$ (Running Max): 当前遇到的最大 logit。$\ell$ (Running Sum): 当前的分母(指数和)。当你算完 Block 1 得到一个“临时结果” $O_{old}$ ,然后处理 Block 2 时,你需要用调整因子来“修正”之前的 $O_{old}$ ,让它看起来像是用全局最大值算出来的一样。公式逻辑(Rescaling):

$$O_{new} = \text{diag}(\ell_{new})^{-1} (\text{diag}(\ell_{old}) e^{m_{old} - m_{new}} O_{old} + e^{S_{new} - m_{new}} V_{new})$$

这里的 $e^{m_{old} - m_{new}}$ 就是你说的调整因子。它的作用: 允许我们在不知道全局 Max 的情况下,先算局部结果,回头再补全(Rescale)。并非“消除依赖实现并行”: 实际上,对于同一个 Query Block,它必须串行地遍历所有的 KV Blocks(Outer Loop / Inner Loop 结构)。FlashAttention v1: 在 Sequence Length 维度上其实没有完全并行化(Split-K),主要并行在 Batch 和 Head 维度。FlashAttention v2: 才真正引入了 Q 维度的并行,进一步优化了这一点。


  1. 模型怎么捕捉位置信息的?

Attention 本身是对序列顺序不敏感的,Self-attention 只看 token 之间的相似度,必须显式注入位置信息。

核心思想:给每个位置(index 0, 1, 2...)分配一个固定的向量 $P$,直接加到词向量 $X$ 上。通常采用 正弦位置编码可学习位置编码 两种。

 - 正弦位置编码 (Sinusoidal): (Original Transformer)不训练,直接用 $\sin$ 和 $\cos$ 函数生成一组固定的波形。不同频率的波叠加,使得每个位置都有唯一的“指纹”。
 - 可学习位置编码 (Learned): (BERT, GPT-2):创建一个大小为 [Max_Seq_Len, Hidden_Dim] 的 Lookup Table(也就是 nn.Embedding)。让模型自己去学第 1 个词应该加什么向量,第 2 个词加什么向量。

外推性差 (Extrapolation): 如果训练时最大长度是 512,模型就不知道第 513 个位置对应的向量长什么样,推理时直接崩掉。
割裂感: $X+P$ 这种相加的方式,在数学上混合了“语义”和“位置”,理论上不够优雅。

$$Attention(Q, K) = \text{Softmax}\left(\frac{QK^T + B_{(i-j)}}{\sqrt{d}}\right)$$

T5 / RPE: 训练一个 Bias 表,根据相对距离查表得到一个标量,加到 Attention Logits 上。
ALiBi (Attention with Linear Biases): (Bloom 等模型)非常硬核,不训练任何参数。直接根据距离给 Attention Score 加上一个负的惩罚项:距离越远,分数扣得越多( $m \cdot |i-j|$ )。优点: 外推性极强,训练短序列,推理长序列效果也很好。

核心思想:将位置信息编码为向量的旋转角度。

直观理解想象 Hidden Dim 是二维平面上的一个向量。

 - 位置 0: 向量不转。
 - 位置 1: 向量逆时针转 $\theta$ 度。
 - 位置 $m$: 向量逆时针转 $m\theta$ 度。

为什么这么做有效?

Attention 的核心是点积(Dot Product):$q \cdot k = |q||k|\cos(\alpha)$ 。点积只关心两个向量的夹角,不关心它们的绝对方向。如果我们把 $q$ 旋转了 $m\theta$(代表位置 $m$ ),把 $k$ 旋转了 $n\theta$(代表位置 $n$)。那么它们做点积时,包含的角度信息就是:

$$(m\theta + \phi_q) - (n\theta + \phi_k) = (m-n)\theta + (\phi_q - \phi_k)$$

结果里自然出现了一个 $(m-n)$ 项!
这意味着:通过对每个 Token 进行绝对位置的旋转,Attention 计算结果自动包含了相对位置信息。

算子层面的实现 (RoPE Kernel)

在 FlashAttention 或者 vLLM 源码中,RoPE 是这样实现的:

 - Pairing: 把 Hidden Dim 切分成两两一组 $(x_1, x_2)$。
 - Rotation: 对每一组应用旋转矩阵:

$$ \begin{pmatrix} x'_1 \ x'_2 \end{pmatrix} = \begin{pmatrix} \cos m\theta & -\sin m\theta \ \sin m\theta & \cos m\theta \end{pmatrix} \begin{pmatrix} x_1 \ x_2 \end{pmatrix} $$

 - In-place: 通常为了省显存,这个操作是直接在 $Q$ 和 $K$ 显存上原地修改的,或者在计算 Attention 前融合在 Kernel 里做。

  1. 长度外推是什么,怎么实现?

长度外推是指模型在较短的上下文长度(如 4k)上训练,但在推理(Inference)时能够处理更长序列(如 16k, 32k 甚至无限长)的能力。

实现暂时不考虑。


  1. DPO (Direct Preference Optimization) 和 PPO (Proximal Policy Optimization) 的异同?

PPO:把语言模型当成一个“做动作的智能体”,通过奖励函数 + 强化学习来慢慢改进。

DPO:不把问题当强化学习,直接用“人类更喜欢哪个回答”来训练模型,跳过奖励模型和复杂的 RL 过程。

特性 PPO (RLHF 传统流派) DPO (新贵流派)
训练流程 复杂 (4阶段):SFT $\to$ RM $\to$ PPO (Actor, Critic, Ref, Reward) 简单 (1阶段):直接在 SFT 基础上用偏好数据对 $(x, y_w, y_l)$ 训练
显存占用 极高。训练时需要同时加载/维护 4 个模型(Actor, Ref, Critic, Reward)。 低。只需要加载 2 个模型(Policy, Ref)。显存约为 PPO 的一半。
稳定性 极差。超参数敏感,容易训练崩(Reward Hacking, KL 爆炸)。 极好。类似 SFT 的监督训练,Loss 曲线平滑。
数学本质 最大化期望奖励 $E[R(x, y)] - \beta KL$ 最小化 $y_{win}$ 和 $y_{lose}$ 之间隐式奖励差值的 Log-Sigmoid Loss
Infra 视角 需要复杂的 Orchestration(在生成节点和训练节点间搬运数据),通信开销大。 只是特殊的 DataLoader + Loss Function,标准的 DDP/FSDP 即可支持。

  1. SFT+DPO模型后,怎么评估的,评估哪些指标?

在大模型领域,评估(Evaluation)通常分为 客观题(Benchmarks) 和 主观题(基于 LLM 的打分)。

A. 通用能力评估 (General Capabilities),防止“对齐税” (Alignment Tax),即防止模型为了变乖而变笨。

B. 对齐与对话能力 (Alignment & Chat),这是 DPO 重点提升的领域。

C. 安全性 (Safety)


  1. 无幻觉率怎么评估?

幻觉(Hallucination)指模型一本正经地胡说八道。这是目前工业界最难评估的指标,因为很难有“标准答案”。

常用评估方法:


  1. 什么是模型的困惑度?

数学定义:PPL 是交叉熵(Cross Entropy)的指数形式。它衡量的是概率分布的不确定性。

$$PPL(X) = \exp \left( - \frac{1}{N} \sum_{i=1}^{N} \log P(x_i | x_{<i}) \right)$$

直观理解

Caution

(面试必考点)PPL 仅适用于预训练(Pre-training)阶段: 在 SFT 或 RLHF 阶段,PPL 变高 往往是正常的,甚至是有益的。PPL 与生成质量不完全正相关: 一个只会重复 "the the the the" 的模型 PPL 可能极低,但生成质量为零。对于 Chatbot,我们需要的是多样性,而不是极致的确定性。


  1. bitsandbytes和accelerator(hugging face)?

这是两个在 LLM 训练和微调中必须要用的库,一个是底层优化,一个是上层抽象。

Accelerator (Hugging Face)
定位: 分布式训练的“傻瓜相机” / 胶水层。
核心功能: 它把繁琐的 PyTorch 分布式代码(DDP, FSDP, DeepSpeed)和设备管理代码封装起来了。
解决痛点:
- 写代码时: 你不需要写 x.to(device),只需要写 accelerator.prepare(model, data)。
- 切换环境时: 同样一套代码,你想从单卡切换到多卡 DDP,或者切换到 TP/FP16 混合精度,不需要改代码,只需要在命令行运行 accelerate config 配置一下即可。
- Infra 价值: 它抹平了不同硬件(CPU, GPU, TPU)和不同并行策略(DDP, FSDP, DeepSpeed)之间的 API 差异。


  1. 什么是量化?常见的量化类型?

量化,就是用“更少的数字精度”来近似表示原本的高精度数值。在误差可接受的前提下,最大化性能收益。

量化是将模型参数(Weights)和激活值(Activations)从高精度(通常是 FP32 或 BF16)映射到低精度(如 INT8, INT4, FP8)的过程。

在深度学习里,最常见的是:

用 INT8 / INT4 等低比特整数,去近似原本的 FP32 / FP16 浮点数。

大模型中,主要量化权重、激活和梯度。量化难度递增,且梯度主要在训练阶段涉及,在 QAT 中才会涉及到。

常见的量化类型:

按映射方式分:

$$Q(x) = \text{Clamp}(\text{Round}(x / S + Z), Q_{min}, Q_{max})$$

其中 $S$ 是 Scale(步长),$Z$ 是 Zero-point(零点)。

按量化阶段分:

按照量化单位区分:

权重通常用 per-channel,激活多为 per-tensor。

按照对称性区分:

LLM 中 更常用对称量化。

Per-Tensor (Layer-wise) 量化做法:

一层里的所有参数(比如 $4096 \times 4096$ 的矩阵),共用这一个 Scale 和 Zero-point。
问题: 离群点 (Outliers) 毁灭打击。假设矩阵里大部分数是 0.1, 0.2,但有一个数是 100。为了包住 100,Scale 会变得很大。结果:0.1 和 0.2 量化后都变成了 0,精度完全丢失。

Per-Channel (Channel-wise) 量化做法:

对权重矩阵的每一行(或每一列),单独计算一个 Scale。如果是 $[C_{out}, C_{in}]$ 的 Linear 层,通常对 $C_{out}$ 维度做量化。
为什么这么做?隔离离群点: 如果第 1 行有个 100,那只有第 1 行的 Scale 很大;第 2 行最大值是 0.5,第 2 行就可以用很细腻的 Scale。
硬件支持: 在做矩阵乘法时,结果是一行一行累加的,Per-Channel 的 Scale 可以在累加完之后再乘回去,计算上完全可行。

Per-Token (Activation 量化)做法:

对激活值 $X$ ( $[Batch, Seq, Hidden]$ ),针对每一个 Token 的 Hidden 向量单独计算 Scale。
原因: LLM 中存在 Activation Outliers 现象(某些特定的 Channel 在所有 Token 上数值都很大)。

Per-Head 量化 (KV Cache 场景)在 Transformer 的 KV Cache 量化中,有时会按 Attention Head 为粒度进行量化。
原因: 不同的 Head 关注的语义模式不同,数值分布差异很大。混合在一起量化会拉低精度。

Per-Group (Block-wise) 量化 (LLM 必知)做法: Per-Channel 可能还不够细。比如把权重每 128 个数分为一组(Group),每组一个 Scale。代表算法: GPTQ, AWQ。代价: 需要存储更多的 Scale(显存占用微增),但能让 INT4 精度接近 FP16。


  1. 什么是蒸馏 (Distillation)?

定义: 知识蒸馏(Knowledge Distillation, KD)是指训练一个**小模型(Student)去模仿一个大模型(Teacher)**的行为。

核心逻辑: Teacher 模型不仅输出最终的预测结果(Hard Label,比如 "Cat"),还输出 Softmax 层的概率分布(Soft Label,比如 "Cat: 0.9, Dog: 0.09, Car: 0.01")。 这个 "Dog: 0.09" 虽然是错的,但它包含了暗知识 (Dark Knowledge):说明这张图里的东西虽然像猫,但有点像狗,绝不像车。Student 学习这种分布能比单纯学习 Hard Label 泛化得更好。

HPC/Infra 视角: 蒸馏通常与量化或剪枝结合。比如,用一个 FP16 的 Llama-70B 作为一个 INT4 的 Llama-70B 的 Teacher,通过蒸馏让量化后的模型恢复精度。

  1. QAT 和 PTQ 什么区别?QAT 具体过程?
特性 PTQ (Post-Training Quantization) QAT (Quantization-Aware Training)
训练需求 不需要重训练。只需少量校准数据(Calibration Data)。 需要重训练(Fine-tuning)。
计算成本 极低(几分钟)。 高(几小时到几天)。
精度 INT8 尚可,INT4 精度下降严重。 精度最高,几乎无损,适合极低比特(INT4/INT2)。
应用场景 大模型(LLM)首选(因为重训成本太高)。 端侧小模型(ResNet, MobileNet)或追求极致精度的场景。

QAT 的具体过程

QAT 的核心思想是:在训练过程中模拟量化带来的误差,让神经网络学会适应这种误差。
插入伪量化节点 (Fake Quantization Nodes):在模型的 Weight 和 Activation 处插入一个“模拟量化”的操作。
前向传播 (Forward): 实际上还是用 FP32 算,但是会强制做一个 Dequantize(Quantize(x)) 的操作,
模拟精度丢失(Rounding Error)和截断(Clipping)。
$$\text{Output} = \text{FakeQuant}(x) \approx x + \text{noise}$$

直通估计器 (STE, Straight Through Estimator):这是 QAT 的灵魂。
问题: Round 函数导数处处为 0,梯度无法反向传播。
解决: 在反向传播时,直接把 Round 函数当作 $y=x$(导数为 1),忽略量化操作,让梯度直接穿透过去更新原始的 FP32 权重。
微调 (Fine-tuning):带着这些伪量化节点进行常规的 SGD/Adam 训练。模型参数会调整,试图去弥补量化噪声带来的 Loss。
图冻结 (Fusion & Freezing):训练结束后,丢掉伪量化节点,将 FP32 权重真正转换为 INT8 权重 + Scale,部署到推理引擎(TensorRT/TFLite)。


  1. 模型训练和推理时用到的数据类型解析?
数据类型 总位宽 符号位 (S) 指数位 (E)(决定范围) 尾数位 (M)(决定精度) 动态范围 (近似) 最小精度 (分辨率) 核心特性 & Infra 视角
FP32(IEEE 754) 32 1 8 23 $10^{-38} \sim 10^{38}$ $\sim 10^{-7}$ 基准。所有类型都以此为参照。训练的主权重通常保存在此格式以累积微小梯度。
FP16(Half) 16 1 5 10 $6 \times 10^{-5} \sim 65504$ $\sim 10^{-3}$ 范围极窄。指数位仅5位,非常容易上溢 (Overflow),因此训练时必须用 Loss Scaler。
BF16(Brain Float) 16 1 8 7 $10^{-38} \sim 10^{38}$ $\sim 10^{-2}$ 为深度学习而生。指数位与 FP32 相同(8位),不需要 Loss Scaler。牺牲了精度换取了训练的稳定性。
TF32(Tensor Float) 19*(存为32) 1 8 10 $10^{-38} \sim 10^{38}$ $\sim 10^{-3}$ Ampere 架构特供。截取 FP32 的8位指数 + FP16 的10位尾数。计算时约等于 FP16 精度,但有 FP32 的范围。
FP8(E4M3) 8 1 4 3 $\sim 448$ (无 Inf) H100 推理/权重。精度稍高,范围窄。通常用于存储权重 (Weight) 和激活值 (Activation)。
FP8(E5M2) 8 1 5 2 $\sim 57344$ 极低 H100 梯度。指数位由4变5,为了匹配 FP16 的范围,防止梯度计算时溢出。通常用于梯度 (Gradient)。
INT8 8 1 N/A N/A $-128 \sim 127$ 整数分辨率 推理主力。没有指数/尾数,纯整数。依赖 Scale 因子还原数值。

  1. YOLO 架构以及输入和输出

核心架构 (Backbone - Neck - Head)
YOLO 的架构通常分为三个部分:

输入和输出:

主要算子 (Infra 视角)
Conv2d / Depthwise Conv: 计算密集型,占 90% 以上 FLOPs。
SiLU (Swish): 激活函数,超越了 ReLU 的主流选择。
Upsample / Resize: 在 Neck 部分用于上采样,通常是 Nearest 或 Bilinear。
Concat / Split: 显存密集型。CSP 结构中有大量的 Split 和 Concat,容易造成显存碎片或带宽瓶颈。
SPPF (Spatial Pyramid Pooling - Fast): 多个 MaxPool 的级联。
Post-Process (NMS): 非极大值抑制。这是 YOLO 推理的阿喀琉斯之踵。
它通常包含排序(Sort)和 IoU 计算,逻辑复杂,难以并行。
Infra 优化点: 在 TensorRT 中通常使用 EfficientNMS Plugin 将其融合到 GPU 上,否则 CPU 后处理会成为瓶颈。


  1. 主流的图像生成模型?
类别 模型代表 核心架构特点 Infra 关注点
GAN (生成) StyleGAN, CycleGAN Generator (反卷积/上采样) vs Discriminator (卷积) 训练不稳定。生成器通常包含大量的 Upsample 操作。
VAE (生成) VAE, VQ-VAE Encoder $\to$ Latent $\to$ Decoder 瓶颈在于 Latent Space 的采样操作。
Diffusion (生成) Stable Diffusion (LDM), DALL-E 2/3 UNet (Conv+Attention) 或 DiT (Transformer) 迭代式推理 (Loop 20-50次)。访存极其密集 (Weights reload)。
Video (生成) Sora, Kling DiT (Diffusion Transformer) + 3D-Patching 显存也是主要瓶颈,涉及 3D Attention (时空注意力)。

  1. Diffusion 模型基本原理

Diffusion Model (扩散模型) 是目前生成式 AI 的核心。它的本质是学习如何把高斯噪声还原成一张图。

两个过程

前向过程 (Forward Process / Diffusion): $x_0 \to x_T$ 。不断往图片上加高斯噪声,直到图片变成纯噪声。这个过程是固定的,不需要学习。

反向过程 (Reverse Process / Denoising):$x_T \to x_0$。模型学习预测每一步加入的噪声 $\epsilon_\theta(x_t, t)$ 。公式直觉: $x_{t-1} = \frac{1}{\sqrt{\alpha_t}} (x_t - \frac{1-\alpha_t}{\sqrt{1-\bar{\alpha}t}} \epsilon\theta(x_t, t)) + \sigma_t z$ 模型不直接生成图,而是预测噪声,然后从当前图中减去这个噪声。

架构:UNet vs DiTUNet (Stable Diffusion 1.5/SDXL): 带有 Skip Connection 的 ResNet 结构,中间穿插 Cross-Attention (接收 Text Embedding)。
DiT (Sora / SD3): 抛弃 UNet,完全使用 Transformer Block 处理加噪后的 Patch。

Infra 痛点Step 循环: 并不是一次 Forward 就能出图,而是要跑 20~50 次 Forward。KV Cache? DiT 如果作为 Video 生成,可能有类似 KV Cache 的需求,但对于 Image Diffusion,通常每次 Step 输入都不同,无法 Cache。


  1. VAE (Variational Autoencoder) 基本原理

VAE 是一种概率生成模型,它在 Stable Diffusion (Latent Diffusion Model) 中扮演了**“压缩机”**的角色。

基本流程

Encoder: 输入图片 $x$,输出潜在变量的分布参数(均值 $\mu$ 和方差的对数 $\log \sigma^2$)。
注意:它不直接输出向量 $z$,而是输出分布。
Reparameterization Trick (重参数化技巧):
直接从 $N(\mu, \sigma^2)$ 采样是不可导的(阻断了反向传播)。
Trick: $z = \mu + \sigma \cdot \epsilon$,其中 $\epsilon \sim N(0, 1)$。这样 $\epsilon$ 是常数,$\mu$ 和 $\sigma$ 可以求导。

Decoder: 输入 $z$,还原图片 $x'$。
Loss 函数

$$Loss = \text{Reconstruction Loss} + \text{KL Divergence}$$

重建损失 (MSE): 保证还原的图和原图像。
KL 散度: 强迫潜在分布 $N(\mu, \sigma^2)$ 接近标准正态分布 $N(0, 1)$,保证 Latent Space 的连续性和紧凑性。

为什么 Stable Diffusion 要用 VAE?直接在 Pixel Space ($512 \times 512 \times 3$) 做 Diffusion 计算量太大。SD 先用 VAE 的 Encoder 把图片压缩到 Latent Space ($64 \times 64 \times 4$)。Diffusion 过程其实是在 Latent Space 进行的。最后用 VAE Decoder 把 Latent 解码回图片。Infra 视角: VAE 的 Decoder 部分通常是 SD 出图最后一步的显存峰值来源(因为要展开成高分辨率的大图),容易 OOM。


  1. 输入网址到看到网页,网络中发生了什么?

这是一个全链路的过程,可以概括为以下几个关键步骤:

URL 解析 (URL Parsing): 浏览器检查输入的 URL 是否合法,提取协议(HTTP/HTTPS)、域名(https://www.google.com/search?q=google.com)、路径等信息。

DNS 解析 (DNS Resolution): 浏览器需要把域名翻译成机器能读懂的 IP 地址。查找顺序:浏览器缓存 $\to$ 操作系统缓存 (Hosts 文件) $\to$ 路由器缓存 $\to$ ISP 的 DNS 服务器 $\to$ 根域名/顶级域名递归查询。最终得到服务器的 IP 地址。

建立 TCP 连接 (TCP 3-Way Handshake): 浏览器向服务器 IP 发起 TCP 连接(三次握手)。SYN (我想连你) $\to$ SYN+ACK (好的,来吧) $\to$ ACK (我来了)。

TLS/SSL 握手 (如果是 HTTPS): 在 TCP 之上建立加密通道(详见下文)。

发送 HTTP 请求 (Send Request): 浏览器构建 HTTP 报文(GET / HTTP/1.1),包含 Header(Cookie, User-Agent 等)发送给服务器。

服务器处理与响应 (Server Processing):负载均衡器 (Nginx/LVS) 分发请求。Web 服务器/应用服务器 (Tomcat/Gunicorn) 处理逻辑,查询数据库。服务器返回 HTTP 响应报文(状态码 200 OK,Body 中包含 HTML/JSON)。

浏览器渲染 (Rendering):解析 HTML 构建 DOM 树。解析 CSS 构建 CSSOM 树。执行 JavaScript(可能会修改 DOM)。生成渲染树 (Render Tree) $\to$ 布局 (Layout) $\to$ 绘制 (Paint)。

断开连接 (4-Way Wave): 如果没有 Keep-Alive,传输结束后断开 TCP 连接(四次挥手)。


  1. HTTP 协议是什么?

HTTP (HyperText Transfer Protocol,超文本传输协议) 是互联网上应用最为广泛的一种网络协议。

层级: 位于 OSI 模型的应用层。
作用: 规定了客户端(Browser)和服务器(Server)之间如何交换文本、图片、视频等数据。
特点:无状态 (Stateless): 协议本身不记录之前的请求信息(所以需要 Cookie/Session 来维持状态)。基于请求-响应模型: 一问一答。明文传输: 数据未加密,容易被窃听。
版本:HTTP/1.1: 引入 Keep-Alive 长连接,目前最通用。HTTP/2.0: 引入多路复用(Multiplexing)、头部压缩,解决队头阻塞问题。HTTP/3.0: 基于 UDP (QUIC),解决 TCP 层面的队头阻塞和握手延迟。


  1. HTTPS 协议是什么?

HTTPS (HyperText Transfer Protocol Secure) 可以理解为 HTTP + SSL/TLS。定义: 在 HTTP 之下、TCP 之上加入了一个安全层(SSL/TLS)。
作用: 为原本明文传输的 HTTP 提供加密通道。端口: 默认使用 443 端口(HTTP 是 80)。
本质: 披着 SSL/TLS 外衣的 HTTP。


  1. HTTPS 如何保证安全?

HTTPS 时序图HTTPS 通过 SSL/TLS 协议 保证了信息安全的三个核心属性(CIA):
机密性 (Confidentiality): 数据被加密,中间人截获也看不懂(防窃听)。
完整性 (Integrity): 数据在传输过程中没有被篡改(防篡改,通过 Hash/MAC 校验)。
身份认证 (Authentication): 确认服务器就是它声称的那个(防伪装,通过数字证书)。HTTPS 握手时序图 (TLS 1.2 为例)这个过程叫“TLS 握手”,目的是安全地商量出一个对称密钥。(注:TLS 1.3 简化了握手流程,只需 1 个 RTT 甚至 0-RTT,但核心逻辑依然是交换密钥)

  1. HTTPS 为什么要使用对称密钥加密通信,为什么不一直使用非对称加密?

这是一个关于性能与功能权衡的问题。HTTPS 采用的是混合加密机制:握手阶段: 使用非对称加密(如 RSA, ECC)来交换密钥。
传输阶段: 使用对称加密(如 AES, ChaCha20)来传输实际数据。为什么不一直使用非对称加密?计算效率(核心原因):非对称加密 (RSA/ECC): 涉及极其复杂的大数乘法和模幂运算,计算量巨大,速度极慢(比对称加密慢 100~1000 倍)。如果每个数据包都用 RSA 加密,服务器 CPU 会瞬间爆满,网页加载会慢得无法忍受。对称加密 (AES): 基于位运算(异或、代换、移位),非常快,且现代 CPU (如 Intel AES-NI 指令集) 都有硬件加速。数据膨胀:非对称加密后的密文通常比明文要长,会导致传输带宽的浪费。长度限制:非对称加密(如 RSA)只能加密比密钥长度更短的数据(例如 2048 位密钥只能加密很少的数据)。如果要加密一张图片,必须把图片切成无数小块分别加密,这在工程上非常低效且难以管理。安全模型:非对称加密的主要优势是解决“如何在不安全的网络上交换密钥”的问题。一旦密钥交换完成,就没有必要继续忍受它的低效了。总结:非对称加密用来**“安全地传送钥匙”,对称加密用来“用钥匙锁箱子传输数据”**。这是结合了非对称加密的安全性(无需预先共享密钥)和对称加密的高效性的最佳方案。


  1. Docker 的原理是什么?Docker 是怎么管理和隔离资源的?

Docker 本质上是:利用 Linux 内核提供的“隔离 + 限制”能力,在同一个内核上运行多个彼此看起来“像独立系统”的进程。

Docker 本身几乎不“管理”资源,它只是一个“Linux 内核能力的高级封装器”:用 Namespace 隔离视图,用 cgroups 限制配额,用 UnionFS 管理文件。


  1. Bank Conflict 在 Shared Memory 中的具体表现?

物理模型:32 个窗口的银行想象 Shared Memory 是一个有 32 个柜台窗口(Banks) 的银行。
Warp 的执行: GPU 的一个 Warp 有 32 个线程,它们像是一个旅行团,同时走到这 32 个窗口前办事。
理想情况 (Parallel Access): 32 个线程,正好每人去一个不同的窗口。此时,银行能在一个时钟周期(Cycle)内同时处理完所有人的请求。
冲突情况 (Serialized Access): 如果其中有 2 个(或更多)线程,非要挤到同一个窗口去办不同的业务(访问不同的地址),这个窗口就忙不过来了。它必须先给第一个人办完,再给第二个人办。这就是“串行化”,也就是 Bank Conflict。

内存地址是如何映射到 Bank 的?
Shared Memory 是按 4-Byte (32-bit) 为单位切分的。映射规则非常简单:按序轮询。地址 0 -> Bank 0地址 1 -> Bank 1...地址 31 -> Bank 31地址 32 -> Bank 0 (回到开头)地址 33 -> Bank 1公式: Bank ID = (Address_Word_Index) % 32

Image

Image

只有“读”才可以。

Image

Image

Image

bank conflict 可以通过 +1 和 xor swizzie 等方式避免:

int swz = lane ^ (lane >> 1);
float x = smem[swz * 32];

  1. TMA (Tensor Memory Accelerator) 是如何彻底改变数据加载方式的?

TMA 是什么?
TMA 是 SM 里的一个独立硬件单元。你可以给它下达一个指令:“把这块全局内存的数据搬到 Shared Memory,具体的地址计算逻辑(比如 2D 裁剪)是你这样这样...”

然后,所有的 CUDA Threads 就可以去睡觉了(或者去算别的东西)。TMA 会在后台利用全带宽把数据搬好,而且不经过寄存器。

TMA 的核心特性:
异步拷贝 (Asynchronous Copy): cuda::memcpy_async 的终极形态。

不占用寄存器: 数据流向是 Global Mem -> L2 -> Shared Mem,完全绕过 Register File。

硬件计算地址 (Address Generation): 这是最骚的操作。

在做卷积或矩阵乘时,我们需要把内存中不连续的数据块(比如图片的一个 Tile,或者矩阵的一列)搬进来。

以前需要 CPU/Thread 算好偏移量。

现在,你可以配置 TMA Descriptor,告诉它 Tensor 的维度、步长(Stride)和 Box 大小。TMA 硬件自动处理越界(Out-of-bound)和地址跳转。

与 mbarrier 结合: TMA 搬运是异步的,线程怎么知道搬完了?通过 mbarrier (Transaction Barrier)。线程等待一个信号,灯亮了就说明数据到了 Shared Memory,可以直接开算。

一段伪代码直觉 (C++ / CUDA):

// 1. 在 Host 端或者 Global Init 阶段创建 TMA 描述符
// 告诉 TMA:我要搬运一个 [1024, 1024] 的矩阵,每次搬 [64, 64] 的块
// 这种复杂的索引计算,以前是 Thread 做的,现在 TMA 硬件全包了
CUtensorMap tensor_map = create_tensor_map(...); 

// 2. Kernel 内部
__global__ void my_kernel(...) {
    // 发送指令:TMA,干活!把对应坐标的数据搬到 smem_ptr
    // 这条指令瞬间完成,不阻塞,不占用寄存器存数据
    if (threadIdx.x == 0) { // 只需要一个线程发号施令
        cde::cp_async_bulk_tensor_2d_global_to_shared(&smem_ptr, &tensor_map, x, y, ...);
    }
    
    // 3. 所有的 Warps 可以先算点别的,或者用 mbarrier 等待数据到位
    mbarrier.wait();

    // 4. 数据已经神奇地出现在 Shared Memory 里了,直接用 WGMMA 指令计算
    wgmma.mma_async(..., smem_ptr, ...);
}

  1. DeepSeek 用了 MoE,什么是 MoE,结构是什么样的?

Image


  1. 用线性层实现简单的 MoE?
    这是一个简化版的 PyTorch 实现,演示核心逻辑:Routing -> Dispatch -> Expert Computation -> Combine。
import torch
import torch.nn as nn
import torch.nn.functional as F

class SimpleMoE(nn.Module):
    def __init__(self, input_dim, hidden_dim, output_dim, num_experts, top_k=2):
        super().__init__()
        self.num_experts = num_experts
        self.top_k = top_k

        # 1. 门控网络 (Router)
        self.router = nn.Linear(input_dim, num_experts)

        # 2. 专家网络 (这里用 ModuleList 存储多个简单的 FFN)
        self.experts = nn.ModuleList([
            nn.Sequential(
                nn.Linear(input_dim, hidden_dim),
                nn.ReLU(),
                nn.Linear(hidden_dim, output_dim)
            ) for _ in range(num_experts)
        ])

    def forward(self, x):
        # x shape: (batch_size, seq_len, input_dim) -> flatten to (B*S, D)
        original_shape = x.shape
        x = x.view(-1, original_shape[-1]) 
        
        # Step 1: Routing
        # logits: (batch_size * seq_len, num_experts)
        router_logits = self.router(x)
        
        # 获取 top-k 的概率和索引
        routing_weights = F.softmax(router_logits, dim=-1)
        top_k_weights, top_k_indices = torch.topk(routing_weights, self.top_k, dim=-1)
        
        # 归一化权重 (让被选中的专家权重和为 1)
        top_k_weights = top_k_weights / top_k_weights.sum(dim=-1, keepdim=True)

        # Step 2 & 3: Expert Computation & Combination
        # 这是一个 naive 实现,实际生产中会使用 scatter/gather 或 masked matmul 优化
        final_output = torch.zeros_like(x)
        
        # 遍历每个 Expert (实际并行训练中不会这样写,而是会把 token 分发出去)
        for i in range(self.num_experts):
            # 创建掩码:找出哪些 token 选中了当前专家 i
            # top_k_indices shape: (tokens, k)
            mask = (top_k_indices == i).any(dim=-1)
            
            if mask.any():
                # 选出对应的 token
                selected_input = x[mask]
                
                # 专家计算
                expert_output = self.experts[i](selected_input)
                
                # 找出对应的权重
                # 我们需要找到当前专家 i 在 top_k 中的位置,从而取对应的 weight
                # (这里简化处理,假设我们能直接对齐,实际代码涉及较复杂的索引操作)
                # 为了演示逻辑,这里直接用加权累加示意:
                
                # 真正的 MoE 实现通常涉及 einsum 或特定的 CUDA kernel
                # 这里仅做伪代码级的逻辑说明:
                # final_output[mask] += expert_output * weight_of_expert_i
                pass 

        # 在实际面试手写中,建议写出 mask 逻辑或者直接用以下更简单的加权形式:
        # (这种写法计算量大,不是稀疏的,但逻辑正确)
        outputs = torch.stack([e(x) for e in self.experts], dim=1) # (N, num_experts, D)
        
        # 构造稀疏权重矩阵
        expert_mask = torch.zeros(x.size(0), self.num_experts, device=x.device)
        expert_mask.scatter_(1, top_k_indices, top_k_weights)
        
        # 加权求和: (N, E, D) * (N, E, 1) -> sum dim 1
        final_output = torch.sum(outputs * expert_mask.unsqueeze(-1), dim=1)

        return final_output.view(original_shape)

  1. 如何实现 Expert 并行 (Expert Parallelism/EP)?

Image


  1. 为什么要做 Residual 连接,Residual 连接解决了什么问题?

Image


  1. expert ffn 是怎样实现的,当我经过 Attention 层之后,有一个 [b,s,h] 的张量,接下来如何实现 MoE?

理解的核心在于:如何将稀疏的逻辑(每个 Token 找不同的专家)转化为密集的矩阵乘法(GEMM)以利用 GPU 算力。

Image

这里输出两个张量,然后准备重排:

expert_idx  ∈ [n, k]   //  i  token 选中的 k  expert
gate_score ∈ [n, k]   // 对应权重softmax / normalized

Image

Image

Image

# 输入 x: [batch * seq_len, hidden_dim]

# 1. Router
gate_logits = self.gate(x) # [N, num_experts]
weights, selected_experts = torch.topk(gate_logits, self.top_k) 
weights = F.softmax(weights, dim=-1)

# 2. 准备做 Grouped GEMM 或 Loop (面试手写简版通常用 Loop 或 Mask)
results = torch.zeros_like(x)

for i in range(self.num_experts):
    # a. 找出属于专家 i 的 token
    # (实际高效实现不会用 Python loop,而是用 CUDA kernel 里的 index mapping)
    batch_idx, nth_expert = torch.where(selected_experts == i)
    
    if len(batch_idx) == 0: continue
    
    # b. 提取 Token (Gather)
    current_tokens = x[batch_idx] # [n_tokens, h]
    
    # c. 专家计算 (FFN)
    # expert_out = Expert_i(current_tokens)
    expert_out = self.experts[i](current_tokens)
    
    # d. 加权 (Scale)
    # 找到对应的权重: weights[batch_idx, nth_expert]
    current_weights = weights[batch_idx, nth_expert].unsqueeze(1)
    expert_out = expert_out * current_weights
    
    # e. 还原 (Scatter Add)
    # 把结果加回到对应的位置
    results.index_add_(0, batch_idx, expert_out)

# 3. 如果有 Shared Expert (DeepSeek)
shared_out = self.shared_expert(x)
final_output = results + shared_out

# 4. Reshape back
return final_output.view(b, s, h)

Image