1 编程与工程基础
1 编程与工程基础
Section titled “1 编程与工程基础”Python 基础语法
Section titled “Python 基础语法”1. python里的函数是不是对象
Section titled “1. python里的函数是不是对象”是。Python 里的函数是对象,而且是一等对象(first-class object)。
- 可以赋值给变量:
f = print - 可以作为参数传入其他函数
- 可以作为返回值返回,闭包和装饰器都依赖这一点
- 可以放进列表、字典等容器中
本质上函数也是运行时创建出来的对象,所以 Python 才能把“行为”像“数据”一样传来传去。
2. python的装饰器是什么, 有哪些常见的装饰器?
Section titled “2. python的装饰器是什么, 有哪些常见的装饰器?”装饰器本质上是一个高阶函数:接收函数,返回一个增强后的函数,用来在不改原函数代码的前提下追加逻辑。
def log_call(func): def wrapper(*args, **kwargs): print("calling", func.__name__) return func(*args, **kwargs) return wrapper
@log_calldef add(a, b): return a + b常见装饰器:
| 装饰器 | 作用 |
|---|---|
@property | 把方法变成属性访问 |
@staticmethod | 定义静态方法,不自动传 self / cls |
@classmethod | 定义类方法,第一个参数是 cls |
@functools.wraps | 保留被装饰函数的元信息 |
@functools.lru_cache | 给纯函数做结果缓存 |
@dataclass | 自动生成 __init__、__repr__ 等样板代码 |
面试里一般还会补一句:装饰器常用于日志、鉴权、缓存、性能统计和重试。
3. 全局锁是什么, python 中多线程有什么问题?
Section titled “3. 全局锁是什么, python 中多线程有什么问题?”全局锁通常指 CPython 里的 GIL(Global Interpreter Lock)。它保证同一时刻只有一个线程执行 Python 字节码。
它带来的核心影响:
- CPU 密集型任务里,多线程通常不能真正利用多核
- 线程切换本身有开销,算力任务甚至可能比单线程更慢
- IO 密集型任务里,线程在等待网络、磁盘时会释放 GIL,所以多线程仍然有价值
- 多线程依然要处理共享变量竞争、锁争用和死锁等并发问题
CPython 3.13 开始实验性支持禁用 GIL(free-threaded mode)
所以常见经验是:
- CPU 密集型优先多进程
- IO 密集型优先线程池或异步 IO
4. 怎么写并行, io 怎么写?
Section titled “4. 怎么写并行, io 怎么写?”先分场景。
- CPU 密集型:用多进程,例如
multiprocessing或ProcessPoolExecutor - 阻塞式 IO:用线程池,例如
ThreadPoolExecutor - 高并发网络 IO:用
asyncio配合异步库,例如aiohttp
示例:
from concurrent.futures import ThreadPoolExecutor
with ThreadPoolExecutor(max_workers=8) as pool: results = list(pool.map(fetch, urls))import asyncioimport aiohttp
async def fetch(session, url): async with session.get(url) as resp: return await resp.text()
async def main(urls): async with aiohttp.ClientSession() as session: tasks = [fetch(session, url) for url in urls] return await asyncio.gather(*tasks)判断原则很简单:看瓶颈是在 CPU,还是在等待外部资源。
5. python 里数据结构有哪些?
Section titled “5. python 里数据结构有哪些?”常见内置和标准库数据结构有这些:
| 数据结构 | 特点 |
|---|---|
list | 有序、可变、可重复,适合顺序存储和随机访问 |
tuple | 有序、不可变,可作为字典键 |
dict | 键值映射,平均 O(1) 查找,工程里最常用 |
set | 无重复集合,适合去重和成员判断 |
str | 不可变字符串,很多场景也要当基础数据结构看 |
collections.deque | 双端队列,头尾插入删除都是 O(1) |
heapq | 堆结构,适合优先队列、Top-K |
collections.Counter | 计数器,快速统计频次 |
collections.defaultdict | 带默认值工厂的字典 |
面试里一般会顺带比较几个高频点:
list和tuple:一个可变,一个不可变dict和set:底层都基于哈希表list适合顺序遍历,set/dict适合快速查找
6. python 里面的进程和线程有什么区别?dataloader 中的 num_workers 你是否了解?
Section titled “6. python 里面的进程和线程有什么区别?dataloader 中的 num_workers 你是否了解?”进程和线程的核心区别是资源隔离和调度粒度不同。
| 维度 | 进程 | 线程 |
|---|---|---|
| 地址空间 | 相互独立 | 共享同一进程内存 |
| 创建和切换开销 | 更大 | 更小 |
| 通信方式 | Queue、Pipe、共享内存等 IPC | 可直接读写共享变量,但要加锁 |
| 是否受 GIL 影响 | 多进程之间互不影响 | CPython 下受 GIL 影响明显 |
| 典型场景 | CPU 密集型 | IO 密集型 |
DataLoader 里的 num_workers 指加载数据时启动多少个 worker 进程。
num_workers=0:主进程自己读数据和做预处理,最稳定,但吞吐最低num_workers>0:启动多个子进程并行做数据读取、解码、变换,再把 batch 送回主进程- 这样做的目的主要是把数据准备和模型训练流水化,减少 GPU 等待数据的时间
- 不是越大越好,过大可能导致进程切换、内存占用和磁盘竞争变严重
实际设置通常看 CPU 核数、磁盘速度、样本预处理复杂度以及 batch size,一般需要压测后定。
Python 业务与并发
Section titled “Python 业务与并发”1. python 多线程 + LLM API 批处理 百万级别的 prerequest 数据,优化代码并发吞吐量仍然受限制,任务速度不稳定,如何优化?
Section titled “1. python 多线程 + LLM API 批处理 百万级别的 prerequest 数据,优化代码并发吞吐量仍然受限制,任务速度不稳定,如何优化?”先判断瓶颈,不要只盯着“线程数不够”。
- 如果是外部 LLM API 场景,常见瓶颈通常是限流、网络抖动、单请求过小、重试风暴和结果落盘太慢,而不只是 GIL
- 优先把同步阻塞请求改成异步 IO 或连接池复用,减少空转等待
- 用有界并发而不是无限堆线程,例如按 QPS、TPM、并发连接数三层限流
- 做真正的批处理:把小请求聚合成合适 batch,减少 HTTP 往返和序列化开销
- 把流程拆成读取、清洗、请求、重试、落盘几个阶段,用队列做流水线,避免一个慢阶段拖死全链路
- 对失败请求做指数退避和幂等重试,避免瞬时失败放大成雪崩
- 监控必须补齐:成功率、P50/P95 延迟、排队时长、429/5xx 比例、每秒 token 吞吐
工程上我会按这个顺序排查:
- 看 API 侧限速和配额是不是硬上限。
- 看单机网络、连接池、DNS、TLS 建连是否反复发生。
- 看 batch 大小是否过小,导致请求开销占比过高。
- 看结果写库/写盘是不是成为尾部瓶颈。
- 再决定是加机器、加异步、还是加缓存和拆分流水线。
2. 调用大模型,使用 sdk 是什么,openai 又是什么
Section titled “2. 调用大模型,使用 sdk 是什么,openai 又是什么”SDK 是 Software Development Kit,本质上是调用某个服务的客户端工具包,帮你封装鉴权、请求构造、重试、流式读取和错误处理。
- 例如 Python 里的
openai包,就是 OpenAI 官方提供的 SDK 之一 - 也可以不用 SDK,直接自己发 HTTP 请求调 REST API
- SDK 的价值是少写样板代码,并统一接口和异常处理
OpenAI 则是提供模型和 API 服务的厂商 / 平台名称。
OpenAI API是服务openaiPython 包是官方 SDKgpt-4.1、gpt-4o这类是具体模型
一句话区分:SDK 是调用方式,OpenAI 是服务提供方。
3. 项目中多线程和批次处理怎么写的
Section titled “3. 项目中多线程和批次处理怎么写的”比较稳妥的写法通常是“切片 + 队列 + 并发 worker + 批量提交 + 统一重试”。
一个常见结构:
- 主线程负责读源数据,按条数或 token 数切片
- 任务进入有界队列,避免内存无限膨胀
- 多个 worker 并发消费任务,请求模型或下游服务
- worker 内部再做小批量聚合,提高吞吐
- 结果进入结果队列,由单独线程或进程统一写库 / 写文件
- 失败任务进入重试队列,超过阈值后进死信队列
核心设计点:
- 批次大小不能只看条数,最好同时看 token、图片数、请求体大小
- 并发数要和外部服务限额匹配,否则只会得到更多 429
- 写入阶段要和请求阶段解耦,不然请求快、落盘慢时整体仍会卡住
- 每个任务要带唯一 ID,便于幂等重试和断点续跑
如果面试官追问,我会补一句:线程池适合阻塞 IO;如果 SDK 支持异步,通常 asyncio + semaphore + batch aggregator 更稳。
4. 生产者 / 消费者是怎么写和设计的?消息队列是怎么设计的?
Section titled “4. 生产者 / 消费者是怎么写和设计的?消息队列是怎么设计的?”生产者 / 消费者模型的目标是把“生成任务”和“处理任务”解耦,削峰填谷,并让系统更容易扩展。
一个基本设计:
- 生产者负责把任务写入队列
- 消费者从队列拉取任务并处理
- 队列要支持确认机制,避免消费者异常退出后任务直接丢失
- 失败任务要有重试策略,超过次数进死信队列
- 队列最好是有界的,避免上游无限堆积压垮内存
设计消息队列时通常看这些点:
| 维度 | 设计要点 |
|---|---|
| 投递语义 | 至少一次、至多一次、精确一次通常是业务语义 + 幂等共同实现 |
| 顺序性 | 是否要求分区内有序,还是允许乱序并行 |
| 吞吐 | 批量拉取、批量确认、压缩、分区扩展 |
| 可靠性 | ack、持久化、重试、死信队列 |
| 幂等性 | 消息 ID、去重键、重复消费保护 |
| 监控 | 队列长度、消费速率、积压时长、失败率、重试次数 |
如果是本机轻量并发,可以直接用 queue.Queue 或 asyncio.Queue;如果是分布式场景,通常会上 Kafka、RabbitMQ、Redis Stream 这类中间件。
C++ 基础
Section titled “C++ 基础”1. cpp 的编译原理和过程
Section titled “1. cpp 的编译原理和过程”C++ 从源码到可执行文件,一般分四步:
- 预处理:处理
#include、#define、条件编译,展开成一个更完整的源文件。 - 编译:把 C++ 源码编成汇编代码,并完成词法、语法、语义分析和优化。
- 汇编:把汇编代码转成目标文件
.o/.obj。 - 链接:把多个目标文件和库链接起来,解析符号引用,生成最终可执行文件或动态库。
面试里常问的补充点:
- 头文件过多会拖慢预处理和编译速度
- 声明能过编译,定义缺失会在链接阶段报错
- 静态链接把库代码拷进产物里,部署简单但体积更大
- 动态链接依赖运行时共享库,体积更小但部署环境要一致
2. 软链接是什么
Section titled “2. 软链接是什么”软链接(symbolic link, symlink)可以理解成一个“指向另一个路径的特殊文件”。
- 它保存的是目标路径,不是目标文件本体
- 访问软链接时,系统会再去解析它指向的真实路径
- 可以跨文件系统,也可以指向目录
- 如果目标被删掉,软链接会变成悬空链接
它和硬链接的区别:
| 维度 | 软链接 | 硬链接 |
|---|---|---|
| 指向对象 | 路径 | inode |
| 能否跨文件系统 | 可以 | 通常不可以 |
| 能否链接目录 | 通常可以 | 通常不可以 |
| 原文件删除后 | 链接失效 | 只要还有硬链接,数据还在 |
工程里软链接常用于版本切换、统一路径入口和模型/数据目录映射。
1. 模型推理部署的时候用的什么框架,对这方面了解多少,vllm,sglang
Section titled “1. 模型推理部署的时候用的什么框架,对这方面了解多少,vllm,sglang”常见推理部署框架包括 vLLM、SGLang、Triton Inference Server、TensorRT-LLM,以及直接基于 PyTorch / FastAPI 的自建服务。
如果问 vLLM 和 SGLang,我会这样答:
vLLM更偏高吞吐 LLM serving,核心优势是 PagedAttention、KV Cache 管理和 continuous batching,适合通用文本生成服务SGLang在 serving 之外更强调程序化推理流程、结构化生成和多步控制,适合把推理逻辑和模型调用一起编排- 两者都在解决“GPU 不能只服务单请求”的问题,本质是把请求调度、缓存复用和批处理做得更精细
部署时我通常会关注:
- 模型格式和精度:FP16 / BF16 / INT4 / INT8
- 单机单卡还是张量并行、流水并行
- KV cache 占用和上下文长度
- 流式输出、并发数、batch 形态
- 指标是 TTFT、吞吐、GPU 利用率和稳定性,而不是只看平均延迟
2. SSE 和 非流式处理是什么?
Section titled “2. SSE 和 非流式处理是什么?”SSE 是 Server-Sent Events,服务端把生成结果按增量 token 或增量文本持续推给客户端。非流式则是服务端等完整结果生成完后一次性返回。
两者区别:
| 维度 | SSE 流式 | 非流式 |
|---|---|---|
| 用户体验 | 首 token 更快,边生成边看 | 要等完整结果 |
| 服务端实现 | 需要长连接、增量输出和中断处理 | 实现相对简单 |
| 适用场景 | Chat、Agent、长文本生成 | 分类、抽取、短回答 |
| 统计指标 | 更关注 TTFT 和流速 | 更关注端到端耗时 |
面试里可以补一句:流式不一定让总耗时更短,但能明显改善体感延迟。
3. vLLM 部署时如何实现 2k tokens/s 级别的吞吐?prefill / decode、continuous batching、并行策略、显存利用率和请求形态通常怎么分析?
Section titled “3. vLLM 部署时如何实现 2k tokens/s 级别的吞吐?prefill / decode、continuous batching、并行策略、显存利用率和请求形态通常怎么分析?”先说判断框架:吞吐问题要区分是 prefill 瓶颈还是 decode 瓶颈。
prefill处理整段输入,计算量大,受输入长度影响更明显decode每步只生成少量 token,但要反复迭代,受 batch 内并发请求数和 KV cache 影响更大
要做到高吞吐,通常从这几层一起做:
- 用
continuous batching,不要等整批齐了再跑,而是让新请求动态插入批次 - 提高显存利用率,把更多活跃请求和 KV cache 留在 GPU 上
- 合理控制
max_num_seqs、max_num_batched_tokens这类调度参数 - 如果单卡放不下或算力不够,再考虑 tensor parallel / pipeline parallel
- 优先压缩无效开销,例如过长 prompt、过碎小请求、频繁冷启动和不必要的数据拷贝
分析时我会看:
| 方向 | 重点问题 |
|---|---|
| 请求形态 | 输入长度分布、输出长度分布、是否大量短请求或超长上下文 |
| 调度策略 | 动态 batch 是否吃满,是否被少数长请求拖尾 |
| 显存 | 权重、KV cache、激活占了多少,是否频繁 OOM 或发生 swap |
| GPU 利用率 | SM 利用率、显存带宽、PCIe / NVLink 传输是否成为瓶颈 |
| 服务行为 | 是否有流式提前返回、取消请求、重试导致抖动 |
一句话总结:想上 2k tokens/s,不是单调“调大 batch”,而是要让请求分布、KV cache、调度参数和并行策略一起匹配硬件。
4. 并发与压力测试如何设置?压测流量模型、输入长度分布、首 token 时延 / 端到端时延 / 吞吐等指标应该怎么定义,如何定位系统瓶颈?
Section titled “4. 并发与压力测试如何设置?压测流量模型、输入长度分布、首 token 时延 / 端到端时延 / 吞吐等指标应该怎么定义,如何定位系统瓶颈?”压测一定要尽量贴近真实线上流量,而不是只测“固定 128 in / 128 out”这种理想样本。
压测流量模型通常要定义:
- 并发用户数或到达率,例如固定并发、阶梯升压、泊松到达
- 输入长度分布和输出长度分布,而不是只给平均值
- 流式还是非流式,是否允许取消请求
- 请求类型占比,例如短问答、长上下文总结、工具调用混合流量
核心指标定义:
| 指标 | 含义 |
|---|---|
| TTFT | Time To First Token,客户端发出请求到收到第一个 token 的时间 |
| 端到端时延 | 请求发送到完整结果返回的总时间 |
| 吞吐 | 单位时间内处理的请求数或生成 token 数 |
| TPS / tokens per second | 更适合 LLM,看生成效率 |
| 成功率 | 2xx 比例、超时率、429 / 5xx 比例 |
| 资源利用率 | GPU 利用率、显存占用、CPU、网络、磁盘 |
定位瓶颈时可以按链路分层:
- 网关层:连接数、超时、SSE 长连接是否堆积。
- 调度层:batch 是否吃满,队列等待是否过长。
- 推理层:prefill 还是 decode 慢,GPU 是否满载。
- 存储和日志层:写日志、落库、埋点是否拖慢主链路。
如果现象是 TTFT 高但 tokens/s 还行,通常更像排队、prefill 或建连问题;如果 TTFT 正常但整体很慢,更多看 decode、长输出和批次拖尾。