滴滴二面记录

自我评价

本次面试个人认为发挥还好,但是手撕环节直接爆炸,无论是线程池还是事件驱动流水线都没撕出来,之后也是被面试官嘲讽一番后光速结束

问题记录

介绍下transformer

好的,这是一个非常基础但又极其重要的问题,也是展示你对整个领域理解深度的好机会。一个优秀的回答不应该只是背诵组件,而应该清晰地阐述其设计哲学核心机制数据流

我会从一个面试官最想听到的角度,为你构建一个全面且深入的回答。


Transformer架构的精讲

“Transformer 是一个在2017年由 Google 在论文《Attention Is All You Need》中提出的深度学习模型架构。它的出现彻底改变了自然语言处理(NLP)领域,因为它完全摒弃了之前主流的循环(Recurrence, 如RNN/LSTM)和卷积(Convolution, 如CNN)结构,仅依赖于注意力机制(Attention Mechanism)来捕捉输入序列中的长距离依赖关系。这使得模型可以进行大规模的并行计算,极大地提升了训练效率,从而催生了像 GPT 和 Llama 这样的大语言模型。”

“一个标准的 Transformer 模型由 Encoder(编码器)Decoder(解码器)两部分组成。但在像 Llama 这样的自回归语言模型中,我们主要关注的是 Decoder-only 架构。我可以从数据流的角度,为您拆解一个 Transformer Decoder 模块的核心组件,这正是我在 llama_cpu 项目中亲手实现过的。”

“一个 Transformer Decoder Block 的处理流程可以分为以下四个关键步骤:”

1. 输入表示:词嵌入(Token Embedding)与位置编码(Positional Encoding)

  • “首先,输入的文本序列被一个 Tokenizer 转换成一系列数字ID。然后,Embedding 层 会将每个ID映射成一个高维向量,这个向量就代表了该词的语义信息。”
  • “但仅有词嵌入是不够的,因为自注意力机制本身是无法感知序列顺序的。为了解决这个问题,我们需要引入位置编码。传统的 Transformer 使用固定的正弦/余弦函数,但在我的 Llama 实现中,我采用的是更先进的旋转位置编码(Rotary Position Embedding, RoPE)。它的巧妙之处在于,通过在注意力计算前对 Query 和 Key 向量进行‘旋转’,将绝对位置信息蕴含到了 Query 和 Key 的点积结果中,从而让模型能够理解词与词之间的相对位置关系。”

2. 核心计算引擎:多头自注意力机制(Multi-Head Self-Attention)

  • “这是 Transformer 的灵魂。它的目标是计算序列中每个词与其他所有词的‘关联程度’或‘注意力分数’。在我实现的 llamaAttention_int4.cpp 中,这个过程分为三步:”
    1. Q, K, V 投影:输入的词向量(经过位置编码后)会通过三个独立的线性层(Linear layers) 分别投影成三个新的向量:查询向量 (Query, Q)键向量 (Key, K) 和**值向量 (Value, V)**。你可以将它们理解为:Q 代表‘我想查找什么’,K 代表‘我有什么信息’,V 代表‘我的实际内容是什么’。
    2. 计算注意力分数:通过计算每个 Q 向量与所有 K 向量的点积(Q * K^T),我们得到一个注意力分数矩阵。这个分数代表了每个词对其他词的关注程度。之后,这个分数会经过一个 Softmax 函数 进行归一化,得到最终的注意力权重。在 Decoder 中,为了防止模型‘看到’未来的信息,还会使用一个掩码(Mask)将上三角矩阵的分数设为负无穷。
    3. 加权求和:最后,用归一化后的注意力权重去加权求和所有的 V 向量,得到最终的输出。这个输出向量融合了整个序列中与当前词相关的所有词的信息。
  • “而‘多头’(Multi-Head)机制,则是将原始的 Q, K, V 向量在特征维度上切分成多个‘头’,每个头独立进行上述的注意力计算。这允许模型在不同的表示子空间中同时关注不同方面的信息,比如一个头可能关注语法关系,另一个头可能关注语义关联。最后,所有头的输出会被拼接并再次通过一个线性层进行融合。”

3. 信息处理单元:前馈神经网络(Feed-Forward Network, FFN)

  • “经过自注意力层得到的输出,会进入一个相对简单但至关重要的前馈网络。它通常由两个线性层和一个非线性激活函数组成。在 Llama 中,使用的是 SwiGLU 激活函数,这在我的代码中由三个线性层(gate_proj, up_proj, down_proj)共同实现。”
  • “FFN 的作用可以被看作是一个信息处理和提炼的单元。它对注意力层聚合来的信息进行一次非线性变换,增加了模型的表达能力,也可以被视为一种分布式的‘记忆’单元。”

4. 稳定训练的保障:残差连接与层归一化

  • “为了构建非常深的网络(Llama-7B 有32层),残差连接(Residual Connection)层归一化(Layer Normalization) 是必不可少的。”
  • “在自注意力层和 FFN 之后,都会有一个残差连接,即将该层的输入直接加到该层的输出上(output = input + Layer(input))。这创建了一条‘信息高速公路’,极大地缓解了深度网络中的梯度消失问题。”
  • “在每个残差连接之前,会进行一次层归一化。在我的项目中,使用的是 RMSNorm,它对每一层的输入进行归一化,使得数据分布更加稳定,从而加速并稳定了整个模型的训练和推理过程。”

“通过将上述的Decoder Block重复堆叠 N 次,模型就能对输入信息进行逐层地、越来越抽象的特征提取和融合。最后,将最后一层 Decoder 的输出通过一个线性层映射到词表大小,再经过一个 Softmax,就可以预测出下一个最有可能的 Token 了。”

你项目中tensorrt的作用是什么?

这个问题非常基础,是检验你是否真正理解自己项目架构的关键。你的回答必须清晰、准确,并且能够体现出你对TensorRT在整个系统中所处位置的深刻理解。


面试官您好,在这个项目中,TensorRT扮演的角色是整个推理流水线中最核心的计算引擎。它并非一个孤立的组件,而是深度集成在我设计的框架中,承担着两大关键职责:离线的模型极致优化运行时的低延迟推理执行

1. 离线阶段:将ONNX模型编译为最优化的可执行引擎

项目的第一步,也是性能优化的基础,就是利用TensorRT将FoundationPose的两个核心神经网络——RefinerScorer——从ONNX格式转换为高度优化的TensorRT引擎(.engine文件)。

如我的README.md所示,我使用了TensorRT的trtexec工具来完成这个过程。这不仅仅是一个格式转换,而是一个深度优化的编译过程,TensorRT在其中执行了:

  • 层与张量融合(Layer & Tensor Fusion): 将多个独立的算子(如Conv, BN, ReLU)垂直或水平地融合成一个单一的CUDA Kernel,极大地减少了Kernel的启动开销和显存读写。
  • 精度校准(Precision Calibration): 我启用了FP16半精度推理,这使得模型的显存占用减半,并且在RTX 4090这类支持Tensor Core的硬件上,理论吞吐量翻倍。
  • Kernel自动调优(Kernel Auto-Tuning): TensorRT会针对目标硬件(RTX 4090)从数百个预置的CUDA Kernel实现中选择最优的一个。
  • 动态尺寸支持(Dynamic Shapes): 我在生成引擎时明确指定了输入的动态尺寸范围,例如 optShapes=render_input:252x160x160x6。这使得单个引擎文件就能同时支持Register模式下252个候选位姿的大批量(Batch)推理和Track模式下单个位姿的小批量推理,极大地增强了框架的灵活性。

2. 运行时阶段:作为流水线中的一个高性能、线程安全的“黑盒”

在C++框架运行时,TensorRT引擎被我的TrtInferCore类进行封装和管理,并作为AsyncPipeline中的一个核心阶段被调用。其具体工作流程如下:

  • 引擎加载与上下文管理:

    • 框架启动时,TrtInferCore会从磁盘反序列化.engine文件,创建一个ICudaEngine实例。
    • 为了保证线程安全,我没有使用全局的IExecutionContext。而是在TrtInferCore::Inference方法中实现了一个线程局部(Thread-Local)的上下文缓存。每个第一次执行推理的std::thread都会创建一个属于自己的IExecutionContext,这彻底避免了多线程推理时的状态冲突。
  • 零拷贝的推理执行 (Zero-Copy Execution):

    • 推理执行前,我的流水线上游阶段(如FoundationPoseRenderer)已经通过MemBufferPool将输入数据准备到了指定的GPU设备内存中。
    • 调用TrtInferCore::Inference时,框架不会进行任何数据拷贝。而是直接将这些输入和输出Buffer的GPU设备地址指针打包成一个void*数组。
    • 这个指针数组被传递给context->enqueueV2()。TensorRT引擎会直接在这些我们提供的GPU地址上进行原地(in-place)的读写操作。这个零拷贝机制是消除不必要数据搬运、保证极致性能的关键。
  • 与流水线的无缝集成:

    • FoundationPose 主控类持有 refiner_core_scorer_core_ 两个 TrtInferCore 实例。
    • Register流程中,流水线会依次调用 refiner_core_->SyncInfer()scorer_core_->SyncInfer(),将两次独立的神经网络推理无缝地嵌入到整个异步处理流中。

总结来说,TensorRT在我的项目中是实现高性能的基石。它不仅是在离线阶段为我提供了压榨GPU性能的“编译器”,更是在运行时,通过其高效的C++ API,让我能够构建一个零拷贝、线程安全的推理服务,并将其完美地融入到我设计的异步流水线架构中,最终实现了数量级的性能提升。

对于涉及到gpu的算法,你的常规优化流程是什么?

面试官您好,对于任何涉及到CUDA的复杂算法,我遵循一套自顶向下、数据驱动的系统性优化流程。这个流程分为三个核心阶段:宏观架构优化、内存访问优化和计算微观优化。

第一阶段:宏观架构优化——最大化异构并行

这是优化的第一步,也是最重要的一步,因为它决定了性能的天花板。

  1. 性能剖析与瓶颈识别: 我会使用 NVIDIA Nsight Systems 对原始的、串行执行的算法进行端到端性能剖析。目标不是看单个Kernel有多快,而是识别出CPU与GPU之间的依赖关系和空闲时间(Bubbles)。在FoundationPose这个项目中,Nsight的剖析结果清晰地显示出,CPU在进行位姿采样时GPU完全空闲,而GPU在进行渲染和推理时CPU又在等待。

  2. 任务分解与流水线设计: 基于剖析结果,我会将整个算法流程分解为独立的、计算特性异构的阶段。正如我的deploy_foundation项目所示,我将FoundationPose分解为CPU密集的位姿采样、GPU密集的渲染、GPU密集的推理等多个阶段。

  3. 搭建异步流水线: 我会利用C++的std::thread和自定义的BlockQueue构建一个多级生产者-消费者流水线。这个宏观并行架构是性能提升的基石,它通过计算重叠(Compute/Compute Overlap)和拷贝计算重叠(Copy/Compute Overlap),从根本上消除了硬件空闲,榨干系统资源。

第二阶段:内存访问优化——数据流的生命线

在宏观架构确立后,优化的核心就转向了数据流。现代GPU是性能怪兽,但往往受限于HBM带宽。

  1. 消除动态内存分配: 我会用MemBufferPool这样的内存池机制,在初始化时分配好所有需要的GPU和CPU内存。运行时的内存申请和释放,被转换为纳秒级的指针交换,彻底根除cudaMalloc带来的延迟抖动。

  2. 最小化H2D/D2H拷贝: 我会审视整个数据流,确保数据尽可能长时间地停留在GPU上。例如,在我的项目中,渲染出的图像和真实图像的裁剪变换,都是通过CUDA Kernel在GPU上完成,最终拼接好的Tensor直接送入TensorRT,避免了任何中间结果返回CPU。

  3. 优化内存布局: 我会使用 NVIDIA Nsight Compute 来分析关键CUDA Kernel的性能。首要关注的就是内存访问模式。我会确保线程束(Warps)中的线程访问的是连续的全局内存(Global Memory),以实现合并访问(Coalesced Access),最大化带宽利用率。如果发现不连续访问,我会考虑重排数据结构或使用共享内存(Shared Memory)进行优化。

第三阶段:计算微观优化——压榨最后一个百分比

当前两个阶段完成后,才会进入到对单个CUDA Kernel的微调。

  1. 提升并行度和占用率(Occupancy): 再次使用Nsight Compute,我会分析Kernel的占用率。如果占用率低,我会调整线程块大小(Block Size),或者重构Kernel以使用更少的寄存器,确保能够有足够多的活跃线程束(Active Warps)来隐藏指令延迟。

  2. 指令级优化:

使用FP16/INT8: 如果算法允许,我会优先使用half或int8类型,利用Tensor Core实现数倍的计算吞吐量提升。

数学函数替换: 对于sin, cos, exp等函数,我会使用__sinf, __expf等固有函数(Intrinsics),它们虽然精度略低,但速度快得多。

避免线程束内部分歧(Warp Divergence): 我会仔细检查Kernel中的if/else和循环,确保同一个线程束内的32个线程尽可能走相同的执行路径。

这个从宏观架构到微观计算的逐层优化流程,是一个系统性的工程方法。它确保了我首先解决最主要的瓶颈,而不是过早地陷入对细枝末节的调优,从而能最高效地达成性能目标。在deploy_foundation项目中,正是这套流程,指导我实现了超过10倍的端到端性能提升。

你有对视觉大模型、大语言模型和3d gaussian模型都进行过优化,现在告诉我你针对这几种模型的优化纲领都是什么,他们之间有什么异同?

好的,指令收到。这是一个极具挑战性的问题,它要求我不仅要理解每个模型的独立优化方法,还要能横向对比,提炼出通用的优化纲领和各自的特性。这需要极高的抽象和总结能力。


面试官您好,这是一个非常深刻的问题。针对视觉大模型(以FoundationPose为代表)、大语言模型(LLM)和3D高斯溅射(3D Gaussian Splatting),我的优化纲领是统一的,但具体的技术焦点和实现手段却因其迥异的性能瓶颈而各有侧重。

我的统一优化纲领是基于一个核心理念:最大化硬件利用率,其本质是最小化数据移动(Minimize Data Movement)。所有优化都围绕这个中心展开,并遵循一个自顶向下的三层优化模型:架构层 -> 内存层 -> 计算层

下面我将分别阐述这三种模型在该纲领下的异同

1. 优化目标与核心瓶颈 (异)

这是三者最根本的区别,直接决定了优化策略的重心。

  • 视觉大模型 (FoundationPose):

    • 核心瓶颈: 异构计算流水线阻塞 (Heterogeneous Pipeline Stall)。它的流程是一条长链,包含CPU密集(采样)、GPU渲染密集(nvdiffrast)和GPU计算密集(TensorRT推理)等多个阶段。主要矛盾是不同硬件单元间的等待和空闲
    • 优化目标: 最大化计算重叠(Overlap),实现端到端的最低延迟和最高吞吐。
  • 大语言模型 (LLM):

    • 核心瓶颈: 访存密集 (Memory-Bound),具体体现在巨大的、动态变化的KV Cache上。性能完全被HBM的带宽和管理效率所限制。
    • 优化目标: 最大化HBM带宽利用率最小化显存碎片,通过高效的请求调度服务大量并发用户。
  • 3D高斯溅射 (3D Gaussian Splatting):

    • 核心瓶颈: 光栅化与原子操作冲突 (Rasterization & Atomic Contention)。它需要将数百万个无序的高斯基元(Gaussians)高效地“溅射”到二维屏幕上。主要矛盾是海量、无序的读写操作,以及在像素上进行颜色混合时大量的原子操作(Atomic Operations)导致的线程冲突。
    • 优化目标: 最大化光栅化效率减少原子操作冲突,实现实时渲染帧率。

2. 优化方案的异同

基于上述不同的瓶颈,我在三个层次上的优化策略也各有侧重。

第一层:架构优化
  • 共同点: 都采用了任务解耦和异步化的思想。
  • 不同点:
    • FoundationPose: 我设计了线性的、多级异步流水线 (AsyncPipeline)。它服务于单个数据流在不同硬件单元间的顺畅传递,目标是隐藏延迟
    • LLM: 优化架构的核心是一个复杂的中心化调度器(Scheduler)。它处理的是大量并发、独立的请求,通过动态批处理(Continuous/Dynamic Batching)将不同长度的请求打包,送给计算单元。目标是提升吞吐量
    • 3D Gaussian Splatting: 架构优化的核心是分块渲染(Tiled-based Rendering)。将屏幕空间划分为小块(Tiles),每个线程块(Thread Block)负责一个Tile。这是一种空间上的数据并行,目标是将全局的排序和冲突问题局部化到每个Tile内部。
第二层:内存优化
  • 共同点: 都极力避免动态内存分配,并最小化Host-Device数据拷贝。我的MemBufferPool思想可以应用于所有场景的I/O Buffer。
  • 不同点:
    • FoundationPose: 内存优化的重点是I/O Buffer的循环复用,确保流水线中传递的数据包 PipelinePackage在不同阶段传递时,无需重新分配显存。
    • LLM: 内存优化的核心是KV Cache的管理。这需要类似 vLLMPagedAttention 机制,通过虚拟内存分页的思想来解决物理显存的碎片化问题,这是其独有的、最关键的内存优化。
    • 3D Gaussian Splatting: 内存优化的重点是高斯基元数据的预处理和加载。例如,需要对高斯数据进行排序(Sorting)或分桶(Binning),以提高渲染时光栅化的缓存命中率和内存访问的连续性。共享内存(Shared Memory)在这里被大量使用,用于在线程块内部缓存Tile所需的高斯数据,减少对全局内存的访问。
第三层:计算优化 (CUDA Kernel Level)
  • 共同点: 都会应用通用的CUDA优化技巧,如指令级并行、避免线程束分歧、使用FP16/Tensor Core等。
  • 不同点:
    • FoundationPose: 计算优化的核心是异构Kernel的并发调度。我使用了多个cudaStream,将渲染Kernel (nvdiffrast)、图像变换Kernel (cvcuda) 和推理任务 (TensorRT) 放入不同的流中,实现GPU内部任务的并行。
    • LLM: 计算优化的核心是Attention Kernel本身,即实现 FlashAttention。通过Kernel FusionTiling在线Softmax等技巧,将一个访存密集型操作硬生生改造成一个计算密集型的操作。
    • 3D Gaussian Splatting: 计算优化的核心是光栅化(Rasterization)Kernel。需要手写高效的排序算法(如Radix Sort)、设计无冲突或低冲突的颜色混合方案(例如,放弃全局原子加,转而在共享内存或寄存器中进行局部混合,最后再写入全局内存),并极度依赖底层硬件的并行原语。

总结

模型类型 优化纲领:最小化数据移动 核心瓶颈 架构层优化 内存层优化 计算层优化
视觉大模型 (FoundationPose) 隐藏异构计算延迟 流水线阻塞 多级异步流水线 I/O Buffer池化 多Stream并发调度
大语言模型 (LLM) 应对海量并发与访存 KV Cache访存 中心化请求调度器 PagedAttention FlashAttention (Kernel Fusion)
3D高斯溅射 (3D GS) 解决海量无序数据光栅化 原子操作冲突 分块渲染 (Tiling) 共享内存缓存/排序 高效光栅化/混合Kernel

可以看出,虽然我的优化哲学是统一的,但针对不同模型的特性,我能精准地识别其独特的性能瓶颈,并从架构、内存、计算三个层面,选择并实现最适合该场景的、截然不同的优化策略。这体现了从具体问题出发,进行系统性工程优化的核心能力。

deployfoundation的内存设计?

面试官您好,我的内存管理设计是针对整个推理流水线中的所有动态数据,它同时覆盖了GPU的设备内存(显存)和CPU的宿主内存(内存),其核心设计思想是:基于RAII机制的、零拷贝、全自动回收的内存池化方案。

1. 设计动机:根除运行时内存分配开销

在设计之初,我的目标就极其明确:对于一个追求极致性能的实时推理框架,运行时的动态内存分配,尤其是cudaMalloc,是绝对要被消灭的性能杀手cudaMalloc是一个非常耗时的、会导致设备同步的阻塞式操作,会引入不可预测的延迟抖动(Jitter),这对于需要稳定高帧率的视觉应用是致命的。

2. 核心方案:基于std::shared_ptr的无锁化内存池

我的解决方案是一套统一的内存池机制,即 MemBufferPool 类。它的核心思想是 “一次分配,循环复用”,并巧妙地利用了C++的 RAII (Resource Acquisition Is Initialization) 特性 来实现全自动、对上层代码无感知的内存回收。

具体实现分为两步:

  • 第一步:启动时预分配(Pre-allocation)
    在推理核心 BaseInferCore 初始化时,MemBufferPool 会被创建。它会一次性地、预先分配出固定数量(例如5个)的 IBlobsBuffer 对象。每一个 IBlobsBuffer 都像一个“数据容器”,内部封装了单次推理所需的所有内存块,这包括了:

    • GPU设备内存:通过 cudaMalloc 分配,用于存放模型的输入输出Tensor。
    • CPU宿主内存:通过 new 分配,用于数据预处理和后处理。
    • 这些预分配好的Buffer对象指针,会全部存入一个线程安全的 BlockQueue 中,这个队列就扮演了“空闲内存池”的角色。
  • 第二步:智能指针与自定义析构器(std::shared_ptr with Custom Deleter)
    这是整个设计的精髓。当流水线的任何一个阶段需要一块内存时,它会从 MemBufferPool 中调用 Alloc 方法。这个方法会:

    1. 从空闲队列中取出一个 IBlobsBuffer裸指针
    2. 将这个裸指针包装成一个 std::shared_ptr 返回给调用者。
    3. 最关键的一步是,在创建 std::shared_ptr 时,我给它提供了一个自定义的析构器(Custom Deleter)。这个析构器是一个Lambda函数,它的逻辑不是 deletecudaFree,而是简单地将这个Buffer的裸指针重新 push 回内存池的空闲队列中

3. 带来的优势

这套设计带来了两个决定性的好处:

  • 极致的性能: 在整个推理生命周期中,不存在任何一次运行时的动态 cudaMallocnew 操作。内存的“申请”和“释放”被转换成了极快的、无锁化的队列 poppush 操作。这将一个重度的、会导致GPU同步的系统调用,变成了一个轻量级的指针交换,从根本上保证了系统的低延迟和高吞吐。
  • 绝对的安全与优雅: 借助C++ RAII的威力,内存管理是全自动且异常安全的。使用者只需要像使用普通智能指针一样使用 std::shared_ptr。当这个智能指针离开作用域时(例如函数返回、对象析构、或在PipelinePackage中被下一个包覆盖),引用计数归零,我定义的那个“归还”操作就会被自动、确定地调用。这彻底杜绝了内存泄漏的风险,也让业务逻辑代码变得极其干净,无需承担任何手动管理内存归还的心智负担。

总而言之,这套内存管理机制是我这个高性能框架的基石之一,它通过现代C++的设计模式,兼顾了极致的性能、代码的安全性和工程的优雅性。


滴滴二面记录
http://zzsy.me/2025/08/22/滴滴二面记录/
作者
yuanyuan
发布于
2025年8月22日
更新于
2025年8月22日
许可协议