Skip to content

1 编程与工程基础

是。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_call
def 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

先分场景。

  • CPU 密集型:用多进程,例如 multiprocessingProcessPoolExecutor
  • 阻塞式 IO:用线程池,例如 ThreadPoolExecutor
  • 高并发网络 IO:用 asyncio 配合异步库,例如 aiohttp

示例:

from concurrent.futures import ThreadPoolExecutor
with ThreadPoolExecutor(max_workers=8) as pool:
results = list(pool.map(fetch, urls))
import asyncio
import 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,还是在等待外部资源。


常见内置和标准库数据结构有这些:

数据结构特点
list有序、可变、可重复,适合顺序存储和随机访问
tuple有序、不可变,可作为字典键
dict键值映射,平均 O(1) 查找,工程里最常用
set无重复集合,适合去重和成员判断
str不可变字符串,很多场景也要当基础数据结构看
collections.deque双端队列,头尾插入删除都是 O(1)
heapq堆结构,适合优先队列、Top-K
collections.Counter计数器,快速统计频次
collections.defaultdict带默认值工厂的字典

面试里一般会顺带比较几个高频点:

  • listtuple:一个可变,一个不可变
  • dictset:底层都基于哈希表
  • 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,一般需要压测后定。

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 吞吐

工程上我会按这个顺序排查:

  1. 看 API 侧限速和配额是不是硬上限。
  2. 看单机网络、连接池、DNS、TLS 建连是否反复发生。
  3. 看 batch 大小是否过小,导致请求开销占比过高。
  4. 看结果写库/写盘是不是成为尾部瓶颈。
  5. 再决定是加机器、加异步、还是加缓存和拆分流水线。

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 是服务
  • openai Python 包是官方 SDK
  • gpt-4.1gpt-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.Queueasyncio.Queue;如果是分布式场景,通常会上 Kafka、RabbitMQ、Redis Stream 这类中间件。

C++ 从源码到可执行文件,一般分四步:

  1. 预处理:处理 #include#define、条件编译,展开成一个更完整的源文件。
  2. 编译:把 C++ 源码编成汇编代码,并完成词法、语法、语义分析和优化。
  3. 汇编:把汇编代码转成目标文件 .o / .obj
  4. 链接:把多个目标文件和库链接起来,解析符号引用,生成最终可执行文件或动态库。

面试里常问的补充点:

  • 头文件过多会拖慢预处理和编译速度
  • 声明能过编译,定义缺失会在链接阶段报错
  • 静态链接把库代码拷进产物里,部署简单但体积更大
  • 动态链接依赖运行时共享库,体积更小但部署环境要一致

软链接(symbolic link, symlink)可以理解成一个“指向另一个路径的特殊文件”。

  • 它保存的是目标路径,不是目标文件本体
  • 访问软链接时,系统会再去解析它指向的真实路径
  • 可以跨文件系统,也可以指向目录
  • 如果目标被删掉,软链接会变成悬空链接

它和硬链接的区别:

维度软链接硬链接
指向对象路径inode
能否跨文件系统可以通常不可以
能否链接目录通常可以通常不可以
原文件删除后链接失效只要还有硬链接,数据还在

工程里软链接常用于版本切换、统一路径入口和模型/数据目录映射。

1. 模型推理部署的时候用的什么框架,对这方面了解多少,vllm,sglang

Section titled “1. 模型推理部署的时候用的什么框架,对这方面了解多少,vllm,sglang”

常见推理部署框架包括 vLLMSGLang、Triton Inference Server、TensorRT-LLM,以及直接基于 PyTorch / FastAPI 的自建服务。

如果问 vLLMSGLang,我会这样答:

  • vLLM 更偏高吞吐 LLM serving,核心优势是 PagedAttention、KV Cache 管理和 continuous batching,适合通用文本生成服务
  • SGLang 在 serving 之外更强调程序化推理流程、结构化生成和多步控制,适合把推理逻辑和模型调用一起编排
  • 两者都在解决“GPU 不能只服务单请求”的问题,本质是把请求调度、缓存复用和批处理做得更精细

部署时我通常会关注:

  • 模型格式和精度:FP16 / BF16 / INT4 / INT8
  • 单机单卡还是张量并行、流水并行
  • KV cache 占用和上下文长度
  • 流式输出、并发数、batch 形态
  • 指标是 TTFT、吞吐、GPU 利用率和稳定性,而不是只看平均延迟

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_seqsmax_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”这种理想样本。

压测流量模型通常要定义:

  • 并发用户数或到达率,例如固定并发、阶梯升压、泊松到达
  • 输入长度分布和输出长度分布,而不是只给平均值
  • 流式还是非流式,是否允许取消请求
  • 请求类型占比,例如短问答、长上下文总结、工具调用混合流量

核心指标定义:

指标含义
TTFTTime To First Token,客户端发出请求到收到第一个 token 的时间
端到端时延请求发送到完整结果返回的总时间
吞吐单位时间内处理的请求数或生成 token 数
TPS / tokens per second更适合 LLM,看生成效率
成功率2xx 比例、超时率、429 / 5xx 比例
资源利用率GPU 利用率、显存占用、CPU、网络、磁盘

定位瓶颈时可以按链路分层:

  1. 网关层:连接数、超时、SSE 长连接是否堆积。
  2. 调度层:batch 是否吃满,队列等待是否过长。
  3. 推理层:prefill 还是 decode 慢,GPU 是否满载。
  4. 存储和日志层:写日志、落库、埋点是否拖慢主链路。

如果现象是 TTFT 高但 tokens/s 还行,通常更像排队、prefill 或建连问题;如果 TTFT 正常但整体很慢,更多看 decode、长输出和批次拖尾。