01. 项目深挖面试题¶
面试官会从简历项目出发,逐层追问设计决策、技术细节、性能指标与踩坑经验。
以下按项目分组,每题附常见追问链和简答思路。
📎 标注的链接指向本 repo 中对应的详细笔记。
一、向量流并行连接引擎(Vector Stream Join)¶
Q1. 请用两分钟介绍一下你做的 Vector Stream Join 引擎,整体架构是怎样的?¶
简答思路: 从"问题 → 方案 → 架构"三步讲:先说大规模高维向量流的实时相似度匹配需求,再说设计了一套三阶段流水线(双流输入 → 窗口增量维护与驱逐 → 结果汇聚),最后点出核心技术点——双层并发索引、SPSC 无锁队列矩阵、插件化策略。
追问链:
- "双流输入 → 窗口增量维护与过期驱逐 → 结果汇聚输出"三阶段各自的线程模型是什么?哪一阶段是瓶颈?
- 为什么选择流水线(pipeline)而不是 BSP 或 MapReduce 风格?对延迟和吞吐的取舍是怎样的?
- 窗口语义具体是 Tumbling Window 还是 Sliding Window?窗口步长和窗口大小的参数如何影响吞吐?
- 整体端到端延迟在什么量级?有没有做过与 baseline(如暴力扫描、单线程)的对比实验?
Q2. 你提到了"双层并发索引结构"(只读全局索引 + 线程局部索引),这是怎么设计的?¶
简答思路: 全局索引保存已稳定的窗口数据,各线程持有局部增量索引处理新到数据;定期将局部索引批量合并到全局索引。查询时先查全局再查局部,通过空间分区路由决定查哪些分片,可变阈值多播控制召回率与计算量的平衡。
📎 并发索引设计思路可参考 并发编程
追问链:
- 全局索引是什么类型(IVF / HNSW / 树结构)?为什么选这种?
- 线程局部索引和全局索引的数据什么时候合并?合并操作的代价是多少?会不会造成查询结果不一致?
- 如果在合并期间有新的查询到达,怎么保证正确性?是 Copy-on-Write 还是读写锁?
- "空间分区路由"具体是怎么做的?数据倾斜怎么处理?
- "可变阈值多播机制"是什么意思?阈值是动态调整的吗?调整策略是什么?
Q3. SPSC 队列矩阵是怎么实现无锁数据交换的?为什么选 SPSC 而不是 MPSC 或 MPMC?¶
简答思路: SPSC 场景下只有一个生产者和一个消费者,只需 acquire/release 语义即可保证可见性,不需要 CAS 竞争,因此吞吐最高。算子之间是固定拓扑的一对一关系,天然适合 SPSC。矩阵指的是 N×M 个算子实例间各有一条 SPSC 通道。
📎 原子操作与内存序详见 并发编程 · 原子操作与内存序
追问链:
- SPSC 队列的底层是环形缓冲区吗?容量满了怎么办——阻塞还是丢弃?
- 为什么说是"矩阵"?算子之间的拓扑是什么样的?每对算子之间有一个 SPSC 队列吗?
- 有没有做过 cache line 对齐(避免 false sharing)?具体怎么做的?
- 无锁队列在 x86 和 ARM 上的内存序语义有什么区别?你用的是
std::memory_order_acquire/release还是seq_cst? - 在实际测试中,SPSC 队列的吞吐瓶颈出现在哪里?
Q4. ConcurrencyManager 统一索引访问接口的设计意图是什么?¶
简答思路: 目的是将并发控制策略与底层索引实现解耦——上层只调创建/注册/查询三个接口,ConcurrencyManager 内部封装读写锁或分段锁策略。这样切换索引实现(如 IVF → HNSW)或改变并发策略时,上层代码零改动。
📎 设计模式思路可参考 设计模式 · 系统设计
追问链:
- 创建、注册、查询三个接口的语义分别是什么?注册和创建有什么区别?
- 并发控制策略具体用了什么?读写锁、无锁结构还是分段锁?
- 如果要切换底层索引实现(比如从 IVF 切到 HNSW),上层代码需要改多少?
- 有没有考虑过索引的生命周期管理?过期窗口的索引怎么回收?
Q5. 工厂模式 + TOML 配置的插件化体系是怎么做的?能不能热加载新的 Join 策略?¶
简答思路: BaseMethod 定义纯虚接口(
build()/search()/update()),每种 Join 策略注册到工厂的 map 中;运行时读 TOML 配置文件中的策略名,通过工厂创建实例。当前不支持动态链接库热加载,但通过配置切换 + 重启即可快速实验。
📎 工厂模式详见 设计模式 · 系统设计
追问链:
- BaseMethod 接口定义了哪些纯虚函数?不同 Join 策略的核心差异在哪里?
- TOML 配置里有哪些关键参数?配置错误时的容错机制是什么?
- 能不能通过动态链接库(.so)的方式热加载新策略?如果做了,怎么做的?如果没做,为什么?
- 这套插件体系和普通的 Strategy Pattern 有什么区别?
二、Sage AI 推理编排框架(中间件开发)¶
Q6. 你用 PyBind11 把 C++ 组件封装成 Python 模块,能讲讲整体做法和遇到的坑吗?¶
简答思路: 核心做法是在 PyBind11 的
PYBIND11_MODULE宏内逐类/逐函数绑定;最大的坑是 C++ 对象生命周期——用shared_ptrholder 让 Python GC 和 C++ 引用计数协作,避免悬挂指针。另一个坑是多扩展模块链接同一 .so 时的符号冲突,通过 CMakePRIVATE链接 +-fvisibility=hidden解决。
📎 智能指针详见 值类别 · 移动语义 · 智能指针
追问链:
- PyBind11 绑定的对象粒度是什么?是函数级、类级还是模块级?
- C++ 侧的异常怎么传递到 Python 侧?有没有遇到过 GIL 相关的性能问题?
- 涉及到 C++ 对象生命周期和 Python GC 的交互时,怎么处理所有权?用了
shared_ptr还是unique_ptr持有? - 你提到"CMake 共享依赖规范",具体是怎么做的?多个扩展模块依赖同一个 C++ 库时怎么避免符号冲突?
- 构建一致性问题具体是什么表现?你是怎么解决的?
Q7. SageFlow 通过 Pipeline 服务化封装,具体是怎么架构的?¶
简答思路: 将 SageFlow 引擎包装为一个 gRPC/HTTP 服务,外部提交 DAG 描述即可启动一条 Pipeline;服务端按 Operator 粒度调度,每个 Operator 是一个独立的执行单元。失败默认重试 3 次,超过阈值则整条 Pipeline 标记失败并回调上游。
📎 RPC 与服务化详见 RPC · 消息队列 · DNS · CDN
追问链:
- Pipeline 的调度粒度是什么——是按 Operator 还是按 Stage?
- 服务化封装用的是什么协议?gRPC / HTTP / 自定义 RPC?
- 一个 Pipeline 里如果某个 Operator 失败了,整个 Pipeline 的容错策略是什么?重试、跳过还是回滚?
- 多个 Pipeline 之间能不能共享中间结果?数据怎么传递?
三、CPU-GPU 协同加速 ANNS 研究¶
Q8. CPU-GPU 协同加速 ANNS 的任务划分是怎么做的?为什么不全放 GPU 上?¶
简答思路: GPU 擅长批量并行的距离计算(如粗筛阶段),但索引更新涉及大量随机内存访问和复杂数据结构操作,更适合 CPU。二者通过异步 CUDA Stream 流水线化——GPU 计算当前批次时,CPU 同时准备下一批次的数据和索引更新,用 pinned memory 加速 PCIe 传输。
📎 并行计算基础可参考 进程 · 线程 · 内存
追问链:
- 哪些计算放在 CPU、哪些放在 GPU?划分的依据是什么?
- CPU 和 GPU 之间的数据传输(PCIe 带宽)是不是瓶颈?你怎么缓解的?
- CUDA kernel 的线程块和网格大小怎么配置的?有没有做过 occupancy 分析?
- 索引更新和查询同时进行时,GPU 上的资源竞争怎么处理?用了 CUDA Stream 吗?
- 实验中召回率和延迟的平衡点是怎么选的?Recall@10 / Recall@100 分别能到多少?
Q9. 你提到"通过解耦合的任务划分,将计算负载合理分配至 CPU 和 GPU",这个解耦具体怎么体现?¶
简答思路: 按索引操作的阶段解耦:粗筛(聚类中心距离计算)在 GPU 批量执行,精排和图遍历在 CPU 执行,索引结构维护(插入/删除节点)也在 CPU。通过双缓冲机制让 GPU 计算和 CPU→GPU 数据拷贝重叠。
追问链:
- 是按索引的不同阶段解耦(如粗筛在 GPU、精排在 CPU),还是按数据分片解耦?
- 任务调度器是自己写的还是用了现成框架(如 CUDA Graph、TaskFlow)?
- 如果 GPU 算力被其他任务占用(多租户场景),你的框架怎么退化?有没有 fallback 到纯 CPU 的路径?
四、自动驾驶仿真器(L4 / AVP / L2)¶
Q10. L4 仿真器的 MVC 架构具体是怎么分层的?引擎动态库和仿真运行时各负责什么?¶
简答思路: Model 是仿真世界的状态(道路、交通参与者、自车位姿等),Controller 是各 Actor 的行为逻辑(轨迹跟踪、交通流控制),View 对应的是对外输出的感知/定位信息。引擎动态库封装 M+C,仿真运行时负责通过中间件向下游发送模拟数据并接收控制回传。
📎 设计模式详见 设计模式 · 系统设计
追问链:
- 引擎动态库是 .so 还是 .dll?为什么用动态库而不是静态链接?
- Controller 怎么调度多个 Actor 的行为?是基于时间步的同步更新还是事件驱动?
- 从 ROS 重构到 Apollo Cyber 中间件,最大的挑战是什么?接口层做了什么抽象来降低迁移成本?
- 动力学模型具体用的什么?简单的运动学模型还是带轮胎力学的?精度和性能怎么平衡?
Q11. Failure Detector 是什么?你做的"信号灯违规检测"具体怎么实现的?¶
简答思路: Failure Detector 是仿真框架中的检测器模块,每帧读取自车状态和环境状态,根据预定义规则判定是否发生违规(如闯红灯、碰撞)。信号灯违规检测的核心是判断自车通过停止线时信号灯的状态——从仿真状态中取自车位置和信号灯相位,做空间+时序的联合判定。
追问链:
- Failure Detector 的输入是仿真器的哪些数据?判定条件是怎么定义的?
- 检测频率是每帧都做还是按事件触发?对仿真性能的影响有多大?
- 你开发了多个不同的 Detector,它们之间有没有公共基类?架构上怎么做到可插拔?
- Detector 的结果是实时报告还是事后分析?怎么和 CI/CD 流水线集成的?
Q12. L2 仿真器中你用 Protobuf 重构了高德地图数据,能详细讲讲吗?¶
简答思路: 原来高德地图数据是 JSON/自定义二进制格式,解析慢且 schema 不明确。改用 Protobuf 后,先定义 proto schema 描述道路拓扑和车道信息,再写工具抓取在线 API 数据并序列化为 .pb 文件,支持任意路段的拼接组合。好处是强类型、版本兼容、序列化速度快。
📎 序列化协议对比可参考 RPC · 消息队列 · DNS · CDN
追问链:
- 原来的地图数据格式是什么?为什么要换成 Protobuf?
- Protobuf 的 schema 是你自己设计的还是复用了已有的?消息嵌套层级有多深?
- "抓取录制并拼接指定路段"是怎么做的?涉及到地图数据的空间索引吗?
- Protobuf 序列化/反序列化的性能在仿真场景下够用吗?有没有考虑过 FlatBuffers?
Q13. 仿真一致性实验是什么?你是怎么通过模块同步信号提升一致性的?¶
简答思路: "一致性"指相同输入的仿真 case 多次运行,输出轨迹的偏差应极小。不一致的根因主要是多模块异步通信的时序不确定性(消息到达顺序不同)。解决方案是在每个仿真步引入 barrier 同步信号,所有模块完成当前步才推进下一步,把异步系统变为逻辑同步系统。
📎 线程同步原语详见 并发编程
追问链:
- "一致性"的定义是什么?相同输入、多次运行的结果偏差在什么范围内算可接受?
- 不一致的根因是什么?是浮点精度、线程调度还是外部依赖的非确定性?
- 你怎么量化一致性的提升(数值指标)?
- 同步信号的粒度是什么?每个模块一个 barrier 还是更细粒度的?
Q14. IDM 交通流模型支持多段轨迹交通参与者生成,具体是怎么做的?¶
简答思路: IDM(Intelligent Driver Model)通过期望速度、安全间距、加速度参数模拟跟车行为。"多段轨迹"指一辆车的路线可由多段路径拼接(如先直行再转弯),段间通过速度和位置的连续性约束实现平滑衔接。每一段独立配置 IDM 参数以模拟不同路况。
追问链:
- IDM(Intelligent Driver Model)的核心参数有哪些?你是怎么标定的?
- "多段轨迹"是指一辆车可以有多个分段的运动轨迹吗?段与段之间怎么衔接?
- 生成的交通参与者和其它仿真 Actor 之间怎么交互?有没有碰撞检测?
- 这个模型和真实交通数据的拟合度怎么评估?
五、综合 / 跨项目追问¶
Q15. 你同时有自动驾驶工业经验和向量数据库研究经验,你觉得这两个领域在系统设计上最大的共通点和差异是什么?¶
简答思路: 共通点是都对延迟极其敏感——仿真器要求帧间稳定延迟,向量搜索要求毫秒级响应,二者都需要精细的并发控制和内存管理。差异在于仿真器追求确定性(一致性),而向量检索是概率性结果(召回率),容忍一定的近似。
📎 系统设计综合可参考 项目问答
追问链:
- 自动驾驶仿真器对实时性的要求和流式向量处理的实时性要求有什么区别?
- 两个领域分别最看重系统的哪个指标(延迟 / 吞吐 / 一致性 / 容错)?
- 如果让你重新设计仿真器的索引模块,你会不会借鉴向量数据库的思路?
Q16. 你的项目涉及大量并发编程,能系统地讲讲你对 C++ 并发编程的理解吗?¶
简答思路: 分三层理解:底层是原子操作和内存模型(
std::atomic+ 内存序),中层是锁和条件变量(mutex、shared_mutex、condition_variable),上层是任务并行(线程池、std::async、无锁数据结构)。选择哪一层取决于对性能和正确性的要求——我项目里 SPSC 队列用底层、ConcurrencyManager 用中层、Pipeline 调度用上层。
📎 完整并发编程笔记见 并发编程
追问链:
- 你在项目中用过哪些同步原语?
mutex、condition_variable、atomic、无锁结构各在什么场景下用? - 你怎么排查死锁和数据竞争?用过 ThreadSanitizer 或 Helgrind 吗?
- C++20 的协程(coroutine)你了解吗?在你的场景下有没有适用的地方?