ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

寒武纪软件岗笔试复盘:从操作系统到AI芯片软件栈的底层考察

寒武纪软件岗笔试复盘:从操作系统到AI芯片软件栈的底层考察 秋招季又到了不少人私信问我寒武纪软件岗的笔试到底考什么。说实话寒武纪2019年秋招这套题在当年AI芯片公司里算是相当有代表性的既考C/C基本功又考操作系统和体系结构最后还要看你对AI芯片软件栈有没有概念。这期内容我重点复盘“试题二”里的后半部分涵盖操作系统、Linux调试、体系结构、算法手写和开放题题型、考点、踩坑点都拆开讲给后面想冲AI芯片方向软件岗的同学做个参考。1. 笔试题第二场的整体结构与答题节奏1.1 第二场题量分布和考察思路我拿到的这场笔试整体结构和第一场类似但是风格有明显变化。第一场偏C/C语法细节和基础数据结构第二场则明显加重了系统层面的考查题量大概在35道左右时间120分钟题型依次是20道不定项选择、8道填空题、4道简答题、2道手写编程题外加1道开放设计题。这里要特别提醒一下“不定项选择”这四个字。寒武纪的软件岗笔试题不会明确告诉你这道题是单选还是多选选项数量也不固定。也就是说你不仅要判断某个选项对不对还要判断这道题到底有几个正确选项。我见过不少同学在这上面吃亏明明知识点会结果少选漏选直接丢分。估计出题人就是想用这种方式筛选出真正能举一反三的人毕竟写代码的时候边界条件和多种可能情况恰恰是决定程序质量的关卡。1.2 考察重点从“语言层面”下沉到“系统层面”第二场的知识权重非常集中。以我复盘的这份卷子来看操作系统和Linux的内容占了差不多30%计算机体系结构约20%C/C深度约25%算法和数据结构约15%剩下的10%是AI芯片软件栈相关的开放性问题。跟国内互联网大厂软件岗笔试相比这个比例最大的不同在于它不考你Java、不考Spring框架、不考分布式设计而是死磕底层。原因其实不复杂。寒武纪做的是AI芯片软件岗的核心工作围绕驱动、编译器、运行时和算子库展开这些工作极度依赖对内存、并发、指令流水线和缓存行为的理解。与其说这是一份“软件工程师笔试题”不如说它是一份“系统软件工程师笔试题”。如果平时只写应用层业务代码遇到这套题大概率会很吃力。2. 操作系统与Linux真题复盘2.1 进程与线程的差异不是背概念是给场景真题里有一道印象很深的题它没有直接问“进程和线程的区别有哪些”而是给了一个多线程服务器模型要求分析在Linux下如果两个线程同时调用fork这个多线程进程的子进程里到底会有几个线程。这题的关键点在于fork只复制当前调用线程到子进程其他线程不会跟着复制。如果在多线程环境下调用fork子进程里只有调用fork的那个线程存活。如果之后子进程里再调用某个函数而这个函数依赖其他线程初始化好的锁状态就极容易死锁。出题人这么问就是想看你对fork和线程模型的理解是不是基于真实运行机制而不是背教材。还有一个选择题直接给了四个选项进程间通信方式中哪种方式适合大量数据传输。很多人的第一反应是socket但socket有协议栈开销管道则需要多次拷贝信号量根本不适合传数据正确答案是共享内存。原因很简单共享内存是所有IPC方式里唯一不需要内核态数据拷贝的方式所以AI芯片底层做张量数据传输时很多实现优先考虑共享内存而不是管道。2.2 死锁的四个条件去年考了今年仍然值得背第二场有一道填空题专门考死锁问的是死锁的四个必要条件是什么。这个名词大家应该都熟——互斥、持有并等待、不可剥夺、循环等待。但笔试光写四句话是拿不全分的后边还跟了一问破坏循环等待条件最常用的办法是什么。答案显然是对资源进行有序分配也就是所有进程按同一个编号顺序申请资源。为什么会用这个方法因为循环等待的本质是多个进程各持有一部分资源然后互相等对方释放。如果所有人按固定顺序申请就不会出现“A等B、B等A”的环。底层软件里驱动开发经常需要管理多个硬件资源用统一的加锁顺序去规避死锁是最务实的做法比什么银行家算法来得可靠得多。这一点当年考过放到今天的系统软件面试中依然可能是考点。2.3 Linux调试与性能分析简答题简答题里有一道是给了一段崩溃日志让你说明定位步骤。日志信息是“segfault at 0000000000000000 ip 00007f...”很多人一看就懵。其实这类问题在Linux C/C开发里太常见了它本质就是空指针解引用。我当时拿到题目第一步写的是用gdb加载core文件然后输入bt查看调用栈第二步是查看崩溃点附近的汇编确认是不是mov指令访问了地址0第三步是检查指针是否在某个分支里没有被赋值就用了。第二个简答题考的是性能分析程序CPU占用率很高但响应很慢如何排查。这类问题没有一个固定公式但是如果按顺序来先用top或htop看CPU核数和进程负载然后用perf record/report采样热点函数再看是不是缓存未命中过高必要时用strace看系统调用是否频繁。我记得这个题在评分标准里分了两档能写到perf和strace的得分明显高于只写“优化代码”这种大话。说白了出题人就是要看到你手上有多少趁手的排查工具。3. 体系结构与C/C深度考点解析3.1 volatile、内存屏障与多线程的关系这场笔试在C/C方向出现了一道比较有意思的多选在嵌入式系统中以下哪些场景必须使用volatile修饰变量。按我记的大概选项有A. 中断服务程序和主循环共享的标志位B. 多线程间用锁保护的共享变量C. 寄存器映射到内存地址的硬件状态寄存器D. 局部循环计数器。正确答案应该是A和C如果你对编译器行为理解得深D在某些极端条件下也成立但要具体情况具体分析。volatile告诉编译器这个变量可能在当前执行流之外被改变所以不要把它优化到寄存器里每次都从内存重新读取。但这里有个巨坑volatile并不保证原子性也不保证内存屏障。在AI芯片驱动里硬件寄存器必须用volatile去读但如果是两个CPU核之间通信光有volatile是不够的需要配合内存屏障或原子操作。出题人把这道题放在这里还埋了一个选项陷阱B选项说“多线程间用锁保护的共享变量”锁内部已经做了正确的内存序处理再加volatile反而画蛇添足。3.2 大小端判断和位域的内存布局填空题有一道考大小端看起来简单在32位小端模式下存储0x12345678最低地址字节是多少。答案大家都会是0x78。真正拉开差距的是后边那道位域题给定一个struct里面有若干bit-field问这个结构体占几个字节并且问某个字段的bit取值是多少。位域在嵌入式驱动里非常常用尤其是硬件寄存器映射。笔试题目大概长这样typedef struct { uint16_t a : 3; uint16_t b : 5; uint16_t c : 8; } RegField;关键点在于编译器会按单元分配位域。在多数平台的默认对齐规则下3 5刚好占一个字节后面8位占第二个字节所以整体是2字节。但如果你把c改成超过8位或者调整字段顺序结果就可能完全不同。处理这类题时我的经验是先看位域类型占多少字节这里uint16_t是2字节再看一个存储单元能不能放得下所有位域——按顺序从低位开始分配放不下就另起一个单元。笔试时至少一半人会在这类题上算错本质是对C内存布局的“土办法”不够熟。3.3 结构体对齐与sizeof的必考题结构体对齐几乎是寒武纪这类偏底层的软件岗笔试必考项第二场也不例外。题目给了一个结构体里面混着char、int、short和一个指针问在64位系统下sizeof是多少。很多人会背“对齐到最大成员大小”这个口诀遇到指针就傻眼因为指针在64位下是8字节但还要考虑每个成员本身的偏移规则。正确做法是逐成员算偏移char占偏移0然后要跳过3个填充字节让int对齐到4short放在偏移8最后指针要从偏移16开始总大小要按最大对齐数8对齐所以最终是24。这道题最大的坑是编译器默认对齐选项如果用了#pragma pack(1)结果直接变成15。做题时要先看题面有没有pack相关提示没有就按默认规则来计算。这块内容看着基础实际上特别能筛人。4. 算法与手写代码题复盘4.1 手写题一两个有序数组的中位数第二场最后有两道手写编程题第一道是“给定两个有序数组求合并后的中位数要求时间复杂度O(log(mn))”。很多人一看中位数先合并再找时间复杂度直接O(mn)虽然能跑但是复杂度不达标。O(log(mn))的关键是二分法。思路不复杂要在两个数组里找到第k小的数每次比较两个数组的第k/2个元素排除掉较小那一侧的前k/2个元素然后k减半循环下去。中位数则是两种总长度的奇偶情况。如果mn是奇数直接找第(mn)/21个如果是偶数找第(mn)/2和第(mn)/21个求平均。这道题考的不只是二分还考临界条件处理数组越界、k/2超出长度时需要灵活调整很多人在手写时没处理好边界逻辑就乱了。另外说一下卷面问题。笔试是纸质的代码要写清楚变量名和缩进。阅卷老师的评分点里一定会看边界判断是否完整。建议写完以后在末尾补充几行注释把自己考虑的边界条件列一下比如“m0时直接取B数组”“k1时取min(A[0],B[0])”这样做其实是在帮阅卷人快速看到你的思考颗粒度。4.2 手写题二LRU缓存第二道手写题是LRU缓存要求设计一个数据结构支持get和put操作get和put的时间复杂度都是O(1)缓存满时淘汰最近最久未使用的项。这道题在互联网公司也是高频题寒武纪软件岗考它大概率是因为驱动和运行时里有很多类似的资源管理场景。标准方案是哈希表加双向链表。哈希表负责O(1)查找双向链表负责O(1)淘汰。关键点在于每次get命中要把节点从链表当前位置摘下来放到链表头部每次put新节点先判断缓存是否已满满了就删除链表尾部节点同时删除哈希表中对应的映射然后插入新节点到头部。手写时有个细节容易出错链表的摘除和插入操作涉及的指针太多了如果不画图写错一个next或prev指向整个链表就废了。我当时的做法是先画出插入、删除、移动三个操作的指针变化图再动手写代码。不要在代码里使用std::list偷懒面试官想看的是你能不能独立实现双向链表而不是依赖STL。就算允许用STL也要能解释清楚list的迭代器在erase之后为什么会失效这个追问频率很高。4.3 开放设计题一个AI推理引擎的任务调度最后一道开放设计题题干大致是在AI推理场景中一个请求会拆成多个算子任务部分算子可以并行执行部分算子存在前后依赖请设计一个任务调度方案。这道题不要求写完整代码但要求画出依赖图、说明数据结构、讲清调度流程。这道题其实考察的是DAG拓扑排序和线程池的结合使用。我当时的答题思路是先把算子依赖关系建成DAG每个节点是一个任务边表示依赖然后用入度表做拓扑排序入度为零的节点可以进入就绪队列就绪队列由线程池消费每个算子执行完更新依赖它的下游节点的入度如果下游入度归零就把它加入就绪队列。再进一步扩展可以提到优先级调度和资源感知调度比如某些算子占用大块显存可以不立即执行而是等显存充足再调度。开放题没有标准答案阅卷看的是系统设计意识。建议在回答时先明确抽象层次再讲数据结构和算法然后说明多线程同步方式比如互斥锁保护就绪队列还是用无锁队列。能写到无锁队列会在印象分上有明显优势。这个问题放到今天看其实就是寒武纪MagicMind这类编译运行时工具链中常见的技术场景只是当年它还是一道概念题现在已经变成实打实的日常工作了。5. 寒武纪软件栈背景题与备考建议5.1 AI芯片软件岗为什么总要问“你对寒武纪了解多少”第二场笔试的简答题最后一道是简述你对AI芯片软件栈的理解。这道题让很多人犯难因为它不是纯粹的技术题还要看你有没有提前了解目标公司。2019年的时候寒武纪的软件栈叫NeuWare面向思元系列芯片包含算子库、编译器、运行时等层次。到了近两年MagicMind这个工具链慢慢成为寒武纪开发者社区里的主要关键词不少做AI推理部署的工程师应该都在开发者社区下载过MagicMind。它的核心价值在于把训练好的模型自动编译成能在寒武纪芯片上高效运行的代码把算子融合、内存复用、指令调度这些脏活累活交给编译器去优化。笔试如果遇到“谈AI芯片软件栈的理解”你应该形成这样一条思路应用层框架TensorFlow/PyTorch在上层往下是模型转换与图优化层再往下是算子编译器与运行时最下面是驱动和硬件。如果能再点出“AI编译器要在正确性和性能之间做很多权衡比如layout选择、算子融合策略、内存分配策略”就已经是很高分的回答了。2019年那会儿大家对AI编译器还没有今天这么熟悉能回答到这个深度的人确实不多。5.2 给下一届秋招同学的备考清单既然已经复盘到第二场我顺手把这场笔试的备考清单整理一下重点科目就是C内存模型、Linux基础操作、体系结构、算法四类外加对AI编译工具链的认知。第一C方向《Effective C》前几章要反复看重点掌握构造/析构/赋值、智能指针、虚函数实现机制、const和static的多层用法。第二操作系统方向进程线程、同步互斥、死锁、虚拟内存、缺页中断、文件系统这几个板块都属高频建议结合《深入理解计算机系统》第8、9章一起学。第三算法方向hot 100题至少要刷两遍特别是链表、二叉树、二分、动态规划、LRU这几种类型。第四针对寒武纪建议花一点时间了解AI芯片软件栈相关的概念不要求会写算子kernel但至少要知道模型从训练框架到芯片执行要经过哪些阶段。5.3 做题时的几条实战经验最后分享几条我在复盘这场笔试时总结出来的经验都是实打实踩坑换来的。第一看到不定项选择时先把它当成多选来做每个选项单独判断真假再选出所有正确项这样可以降低漏选概率。第二所有涉及内存布局的题目先确认系统位数、编译器默认对齐、有没有pack指令这三个变量不确定答案都免谈。第三手写代码时先写边界条件再写主体逻辑最后写测试用例如果时间不够边界条件注释也能让阅卷人知道你有这个意识。最重要的一条不要只看标准答案要研究答案背后的运行机制。寒武纪软件岗的笔试题目风格跟写业务代码那套完全不同它更看重你是否理解程序在硬件上是怎么跑起来的。如果只会调函数、不会分析内存和并发问题即使笔试过了后面面试追问“为什么”“如果换成这种场景会怎样”时还是会露馅。我事后复盘有个很深的感受这套卷子里没有一道题是为了难而难每道题都能在寒武纪实际的驱动、编译器或者运行时工作中找到对应的影子。准备这种偏底层的软件岗笔试最有效的方式不是刷海量的面经而是老老实实把C/C、操作系统、体系结构这三块基础打扎实再认真研究一下AI芯片软件栈的发展脉络——从NeuWare到MagicMind理解它们解决的问题基本上就抓住了这家公司软件岗笔试的命脉了。
返回列表