news 2026/9/8 6:54:56

寒武纪软件岗笔试题复盘:从C++内存布局到AI芯片推理引擎

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
寒武纪软件岗笔试题复盘:从C++内存布局到AI芯片推理引擎

2019年秋天,我坐在寒武纪软件岗的笔试考场里。那会儿AI芯片公司正是最热的风口,寒武纪作为国内做AI芯片的代表企业,一场软件岗笔试能吸引几百号人投简历。拿到卷子翻了翻,第一感觉是:这不像互联网大厂那种全考算法题的风格,而是把C/C++、操作系统、体系结构、算法、深度学习软件栈全塞进了一套卷子里,题量不小,时间非常紧。当时我觉得这套题考得有点“杂”,后来这些年自己做了推理引擎相关的工作,回头看才发现,这套“试题(二)”里的每一道题,几乎都踩在AI芯片软件栈的核心知识点上。这篇文章就按我记得的题目顺序完整复盘一遍,重点讲清楚每道题背后的原理、我当时的答题思路,以及站在今天(结合寒武纪开发者社区里能下载到的MagicMind推理引擎)该怎么理解这些考点。

1. C/C++与内存布局:一道虚函数送分题背后的三连问

1.1 原题回忆:sizeof到底怎么算

这套卷子里C/C++部分第一题我就印象很深,因为它不是直接问“虚函数是什么”,而是给了一段类定义,让算sizeof。原题大概是这样的:

class Base { public: virtual void f() {} virtual void g() {} int a; char c; }; class Derived : public Base { public: void f() override {} long long b; };

问题分两问:第一问是sizeof(Base)和sizeof(Derived)分别是多少;第二问是调用Derived d; d.f();时,程序是怎么找到Derived::f()这个函数体的。

先说答案。在64位Linux、Itanium C++ ABI、默认8字节对齐的环境下,sizeof(Base)是24,sizeof(Derived)也是24。为什么?因为Base里有一个隐藏的虚表指针vptr,占8字节,int a占4字节,char c占1字节。8加4加1等于13,13向上对齐到8的倍数,就是16?不对,这里要重新算。

等等,让我重新推导一下。Base的布局:vptr在偏移0,占8字节;a在偏移8,占4字节;c在偏移12,占1字节。末尾padding到对齐边界。类的对齐要求是最大成员对齐,vptr是8字节对齐,所以Base末尾要补到16的整数倍?不,类是8字节对齐,13补到16。所以sizeof(Base) = 16。不对不对,我再想一下。

实际上64位Linux下Base应该是16字节。我前面口算说24是错的。Derived呢?它继承了Base的vptr、a、c,加上自己的long long b。布局是:vptr偏移0(8字节),a偏移8(4字节),c偏移12(1字节),这时候为了long long b的8字节对齐,需要跳过3字节padding,b偏移16到23。整块大小是24,对齐是8,所以sizeof(Derived) = 24。

这题真正的考点不是让你背答案,而是看你能不能把内存布局推导出来。我在考场上就是先在草稿纸上画出偏移量,逐个成员排位置,最后才填答案。这里有两个最常见的错误:第一,忘了类里有隐形的虚表指针;第二,忘了long long要求8字节对齐,前面要补padding。32位环境下vptr只有4字节,答案又不一样,所以这种题一定要注明平台,不注平台直接甩答案的基本可以判定为不严谨。

1.2 虚函数调用是怎么路由的

第二问,d.f()的调用过程,这是C++面试里最经典的“八股”之一,但寒武纪这题问得更细一点,它让你画出步骤。我的回答是分三步:

首先,编译器看到d.f(),因为f是virtual函数,它不能直接静态绑定到Derived::f(),而是生成一条间接调用指令,从对象d的起始内存处取出vptr。这个vptr指向Derived类的虚函数表,表里的第0个槽位在构造Derived对象时已经被填成了Derived::f()的地址,最后通过call [vptr + 槽位偏移]完成跳转。

刚学C++的人容易误以为虚函数表是存在对象里的,其实对象里只有vptr,虚函数表是每个类一份,编译期就生成好了,放在只读数据段。继承的时候,如果子类重写了某个虚函数,子类的虚表中对应槽位就会被覆盖成子类函数指针;没有重写的虚函数,槽位会原样拷贝父类的指针。这也是为什么虚函数调用比普通函数调用贵一点点,它多了一次间接寻址,但这个开销在高性能代码里通常可以忽略。

另一个隐藏考点是:为什么Base和Derived的sizeof差别这么大?Derived只比Base多了一个long long,但大小却从16变到24,多出来的那8个字节几乎全是padding和内存对齐的代价。这种题放到AI推理引擎的工程场景里就有实际意义了——如果你在算子实现里定义了一个包含虚函数、又塞了很多小成员的类,并且频繁地创建、拷贝、移动它,内存占用翻倍、cache命中率下降是实实在在的性能杀手。

1.3 我当时踩过的坑:只背结论,不推过程

说实话,这种题我在笔试前刷过很多,但第一次在草稿纸上认真推导成员偏移的时候才发现自己以前很多结论是半懂不懂的。比如我一度以为所有类的对象里都应该先放vptr再放别的成员,其实虚继承和多继承场景下vptr不止一个,布局规则还更复杂。

我给后来者的建议是:不要背sizeof(Base) = 16这种具体数字,而是每次都在草稿纸上老老实实画一遍偏移图。你把这个过程练熟了,哪怕考场上遇到一百个成员变量的类也不慌。另外可以记住一个口诀:对象大小等于最后一个成员偏移加上自身大小,再向上对齐到整个类的对齐数;整个类的对齐数等于最大成员对齐数(通常是最大标量成员的大小)。

2. 一道矩阵转置题,把缓存命中率考到底

2.1 原题回忆:两种循环顺序,性能差了几十倍

这题我印象特别深,因为当时很多同学出了考场都在对答案。原题是:有一个1024×1024的float矩阵A,按行优先存储在内存里,现在要把A转置到同样大小的矩阵B里(B[j][i] = A[i][j]),请你写出两种循环实现,并分析哪一种更高效,为什么。

第一种是最直觉的写法:

for (int i = 0; i < n; ++i) for (int j = 0; j < n; ++j) B[j][i] = A[i][j];

第二种是把内外层循环交换:

for (int j = 0; j < n; ++j) for (int i = 0; i < n; ++i) B[j][i] = A[i][j];

这两种写法访问的A和B的存储位置完全不同。A按行优先存储,所以A[i][j]的地址是A + i * n * sizeof(float) + j * sizeof(float),同一行内地址连续。第一种写法里,外层是i,内层是j,A是按行连续访问的,缓存命中很好;但B[j][i]这一侧就惨了,内层i变化的时候,B的地址每次跳一整行的跨度,也就是每次写B都要把一个cache line换进换出,产生大量缓存缺失。

现代CPU的cache line一般是64字节,一个float占4字节,也就是说一个cache line能装下16个float。理想情况下,你连续读16个float,只需要一次内存访问;但如果每次都要跳到下一个cache line才能拿到一个float,那内存带宽就全浪费在读缓存缺失上了。这个题的差距在1024×1024的规模下能有多大?我做过类似的实验,第一种写法比第二种慢几十倍是常态,如果矩阵再大一点,差距会拉得更开。

2.2 正确答案:分块转置(blocked transpose)

要同时让A和B都获得较好的cache局部性,标准解法是分块。我们把矩阵切成BLOCK×BLOCK的小块,一次处理一块,让这块A的块和B的块都能尽量留在cache里。

#define BLOCK 16 for (int i = 0; i < n; i += BLOCK) { for (int j = 0; j < n; j += BLOCK) { for (int ii = i; ii < i + BLOCK && ii < n; ++ii) for (int jj = j; jj < j + BLOCK && jj < n; ++jj) B[jj][ii] = A[ii][jj]; } }

这里BLOCK取16,是因为16个float正好占64字节,等于一个cache line。你再看这个访问模式:A[ii][jj]在块内按行连续读,每读16个float就换一行;B[jj][ii]在块内按列写,虽然地址不连续,但整个块只有16×16=256个float,也就是16个cache line,L1 cache完全装得下。所以A侧的连续读和B侧的小块写入都能吃到cache红利。

我考场上写的是16×16分块,还顺手在注释里写了“BlOCK大小应根据目标平台cache line大小调优”。后来我在自己的项目里测过,这个参数确实不是越大越好,过大的BLOCK会导致B块的cache line被提前逐出,太小又分块开销太大。一般在8到32之间选,具体要跑benchmark才知道。

2.3 为什么寒武纪要考这个:访存模式就是算子的命

这道题放在AI芯片公司的笔试卷里不是偶然。你去看寒武纪开发者社区里MagicMind的文档,里面大量提到算子的内存布局优化、数据排布转换、算子融合——所有这些的底层,都和这道矩阵转置题是同一个问题:数据在内存里怎么放,决定了访存效率。

比如卷积运算里常用的im2col优化,把卷积展开成矩阵乘法,本质上就是一次大范围的布局重排。展开后的矩阵如果按行访问友好,整个GEMM的访存效率就上去了。MagicMind在做算子融合的时候,会把conv后面的batch norm、ReLU合并成一个算子,这个操作一方面减少了kernel launch的CPU开销,另一方面也让中间张量不需要写回内存再读出来,等于在软件栈层面做了一次“分块转置”。

所以这道题看似在考cache,其实是在考你屁股有没有坐在“AI芯片软件栈”这张椅子上。你能从矩阵转置想到算子布局、想到内存复用、想到推理引擎的图优化,说明你是真理解这个行业的性能瓶颈在哪。

3. 并发题不复杂,但死锁排查思路要清晰

3.1 原题回忆:两个线程互相等锁,怎么判断是不是死锁

操作系统部分的题里有一道让我当时犹豫了很久:假设线程A持有锁L1,等待锁L2;线程B持有锁L2,等待锁L1。问这是不是死锁?如果不是,为什么;如果是,怎么解决?

这是一个经典的“哲学家吃面”式场景。按死锁的四个必要条件去套:互斥条件满足——L1和L2都是互斥锁;持有并等待满足——A拿着L1等L2,B拿着L2等L1;不可剥夺满足——两个线程都不能强行抢对方手里的锁;循环等待满足——A等B,B等A,形成一个环。四条件全满足,所以这确实是死锁。

我当时是先写结论,再逐个条件证明,最后给出解决方案。这个答题结构很稳,因为阅卷人一眼就能看出你是真的懂死锁,而不是背了个概念。

解决办法我写了几种:第一,规定全局加锁顺序,比如所有线程都先拿L1再拿L2,这样A先拿L1等L2,B也必须先拿L1,B拿不到L1就不会拿着L2去等L1,循环等待被打破;第二,用pthread_mutex_trylock尝试加锁,拿不到就释放已有锁,过会儿重试;第三,用带超时的锁,避免永久阻塞。放在工程里,最常用也最推荐的是第一种,加锁顺序不统一是死锁最常见的根因。

3.2 另一道:两个线程交替打印奇数和偶数

并发部分还有一道手写题:开两个线程,一个线程打印奇数,一个线程打印偶数,要求交替输出1、2、3、4……一直到100。这题考的是条件变量和互斥锁的配合。

我的实现思路是用一个互斥锁加一个条件变量,再加一个共享的当前计数。伪代码大概是:

std::mutex mtx; std::condition_variable cv; int num = 1; void print_odd() { while (num <= 100) { std::unique_lock<std::mutex> lock(mtx); cv.wait(lock, []{ return num % 2 == 1; }); if (num <= 100) std::cout << num << std::endl; ++num; cv.notify_all(); } } void print_even() { while (num <= 100) { std::unique_lock<std::mutex> lock(mtx); cv.wait(lock, []{ return num % 2 == 0; }); if (num <= 100) std::cout << num << std::endl; ++num; cv.notify_all(); } }

关键点在于:wait不能单独用,必须配合一个条件谓词,防止“虚假唤醒”。我见过很多人写if (pred) wait()而不是while循环等,多线程环境下很容易出bug。条件变量的正确用法一定是while循环加谓词,这是《C++并发编程实战》里反复强调的。

3.3 从笔试题到推理引擎的并发控制

这套并发题放到现在的AI推理场景里也很好理解。你部署一个模型,多个请求同时打进来,推理引擎往往用多个执行流(stream)并行跑不同batch的算子,每个流内部还会做算子间pipeline。如果两个流同时要访问同一个内存池分配器,申请中间张量内存,分配器不加锁或者加锁顺序不统一,很容易出两类问题:一类是数据竞争,一类是死锁。

MagicMind在管理推理任务时,一个比较核心的设计就是尽量把并发控制下沉到图级别:一个执行流对应一组算子依赖关系,所有算子在同一个流上严格按序执行,天然避免了锁竞争。跨流之间的同步用event,而不是多个互斥锁互相嵌套。这就是“避免死锁”思想在产品里的体现。我现在带新人的时候经常说:笔试里的死锁题不是让你背四个条件,而是让你培养一种嗅觉——看到多个锁同时出现在一个函数里,第一反应就应该是审视加锁顺序。

4. 推理软件栈的选做题:从静态shape到计算图内存复用

4.1 原题回忆:算卷积输出尺寸和参数量

这套卷子的深度学习部分明显是选做题风格,不要求你会训练模型,但要求你对卷积神经网络的基本计算滚瓜烂熟。原题我记得是:输入是224×224×3的图像,卷积核大小是7×7,stride=2,padding=3,输出通道数是64,问输出特征图的宽高是多少,这个卷积层有多少个参数。

卷积输出尺寸公式是out = (in + 2 * padding - kernel_size) / stride + 1。代进去:(224 + 2 * 3 - 7) / 2 + 1 = 448 / 2?,先算224 + 6 - 7 = 223,223 / 2 = 111.5,向下取整是111,111 + 1 = 112。所以输出是112×112。

等等,这里要小心整数除法。223除以2在浮点里是111.5,卷积输出尺寸的公式要求如果除不尽,实际是向下取整再加上1,所以111.5向下取整是111,再加1等于112。或者换一种表达:一般深度学习框架里这个计算是floor((in + 2p - k) / s) + 1,即先floor再+1。112没问题。

参数量就是kernel size乘以输入通道数再乘以输出通道数,再加bias:7×7×3×64 + 64 = 9408 + 64 = 9472。如果不用bias就是9408。这题我猜寒武纪的真正意图是看你知不知道卷积参数和特征图尺寸是两码事,很多人把二者混在一起算,稀里糊涂就算错了。

4.2 另一道更值钱的题:一张计算图的中间张量,内存能不能复用

还有一道比较开放的题,我现在回想起来觉得是整套卷子的精华。题目给了一个很简单的计算图:输入A经过Conv得到B,B经过BatchNorm得到C,C经过ReLU得到D,D再经过一个Pooling得到E。问:在推理时,哪些中间张量的内存在时间上是不重叠的,可以进行内存复用?

这道题的本质是张量生命周期分析。把计算图画成DAG,每个节点(算子)有输入张量和输出张量,每个张量从被生产者写出的那一刻开始存活,到最后一个消费者读它的那一刻结束。内存复用的规则很简单:两个张量的生命周期如果不重叠,就可以复用同一块内存。

在这个例子里,B被BatchNorm消费后,BatchNorm的结果是C。B的最后一个消费者是BatchNorm,D出来后C的使命也就结束了。所以B、C、D三个张量的生命周期首尾相接但互不重叠,理论上可以共用同一块显存。A是输入,E是最终输出,一般单独分配。

这就引出了推理引擎里一个极其重要的概念:内存规划(memory planning)。在静态shape推理里,所有张量的形状是编译期确定的,内存规划器可以在模型编译时把所有中间张量一次性分配好,统一放进一个内存池,按生命周期分析结果复用。这就是为什么静态shape的推理引擎往往比动态shape省显存——动态shape下无法在编译期确定张量大小,只能运行时动态分配,内存碎片和显存占用都会显著上升。

MagicMind在这方面的做法我后来研究过一些:它会在模型编译阶段做完整的图优化和目标平台内存规划,同时把算子融合也纳入规划流程。比如Conv+BatchNorm+ReLU如果融合成一个算子,那么B、C、D这些中间张量在物理上就不存在了,直接在寄存器或片上缓存里完成整个链路的计算,连写入全局内存都省了。这比我笔试时写的“复用同一块内存”又高了一个层次——最优的复用就是不分配。

4.3 如果你当时没学过深度学习,这题怎么蒙

这套卷子是软件岗,不是算法岗,所以其实有不少非AI背景的同学也来考。他们看到卷积计算题会慌,但其实这题给分很良心:只要你写出公式,把数字代进去,过程分就能拿大半。参数量那题更是纯算术题,不知道卷积在干什么也能算。

我的建议是,投寒武纪这类AI芯片公司的软件岗,哪怕你简历上完全没写过深度学习,也一定要把卷积层的输出尺寸公式、池化尺寸公式、全连接的计算方式搞清楚,另外至少知道ReLU、BatchNorm是干什么的。这些东西花一个晚上就能看完,但在笔试里能救你至少十几分。

5. 手撕层序遍历:笔试算法题的性价比选择

5.1 原题:二叉树层序遍历,要求逐层输出

算法题部分我记得有一道很典型的二叉树层序遍历:给定一个二叉树,返回它按层序遍历得到的节点值,要求每一层的节点单独放一个vector,即返回类型是vector<vector<int>>

这题我在LeetCode上刷过,但寒武纪的题目要求稍微严一点,必须逐层输出,不能用普通的单队列一把梭。正确做法是用BFS加一个层级标记:

vector<vector<int>> levelOrder(TreeNode* root) { vector<vector<int>> res; if (!root) return res; queue<TreeNode*> q; q.push(root); while (!q.empty()) { int levelSize = q.size(); vector<int> level; for (int i = 0; i < levelSize; ++i) { TreeNode* node = q.front(); q.pop(); level.push_back(node->val); if (node->left) q.push(node->left); if (node->right) q.push(node->right); } res.push_back(level); } return res; }

关键点就在int levelSize = q.size()这一行。很多第一次写的人会在while循环里直接pop直到队列空,结果所有层的节点混在一起,完全没法区分。你要先记下当前队列里有多少个节点——这些就是当前层的全部节点——然后for循环把这一层处理完,再进入下一轮while循环。这个手法不只在二叉树上有用,凡是需要“按批次处理”的BFS场景都是同一个套路,比如求二叉树最小深度、判断一棵树是否是完全二叉树、多叉树的层序遍历。

5.2 进阶变体:之字形遍历

笔试卷子在这题下面还有一个小问(或者说是口试追问):如果要求之字形遍历,第一层从左往右,第二层从右往左,第三层又从左往右,怎么办?

两种常见思路。第一种是用一个deque,奇数层从尾部pop节点、把子节点按左到右push到头部;偶数层反过来。这个实现比较绕,容易写错。第二种更简单:还是按正常的层序遍历把每层结果放进vector,最后遇到偶数层就把这个vector reverse一下。虽然reverse多花O(n)时间,但代码简单、不容易出错,笔试场景下推荐第二种。

5.3 我的时间分配策略:基础题优先,压轴题看情况

这套算法题的整体难度其实不算高,没有那种一眼望不到头的hard题。但我还是要提醒一句:不要把时间全砸在这里。整张卷子前面有C++、操作系统、体系结构、深度学习一堆题,每道都是分,算法题哪怕只写个暴力解也能拿不少过程分。

我当时的时间分配大概是:C/C++和体系结构部分用了一半多一点的时间,操作系统的并发题用了四分之一,剩下不到半小时给算法题。层序遍历这种送分题先拿到手,再看后面的变体题有没有思路,有思路就写,没思路就把核心函数签名和BFS框架写出来,保证不让阅卷人觉得你完全不会。

我还想多说一句:笔试题里的算法题,和你在LeetCode上刷题不太一样。笔试题更看重“能不能快速写出一个正确、可运行的版本”,而不是“能不能写出最优雅的最优解”。所以平时刷题不要总在hard题上死磕,中等题、尤其是树、链表、栈、队列、二分、动态规划这些高频类目,做到看到题目就能条件反射地搭出框架,才是性价比最高的准备方式。

6. 当年这套题,放到今天依然能打:复盘与备考建议

6.1 这套卷子的核心逻辑:考的是AI芯片软件栈的思维底座

整套卷子考完,我的第一感受是“杂”,第二感受是“实在”。它不考你背了多少机器学习模型的细节,也不考你刷了多少道LeetCode,而是想确认你有没有能力在AI芯片的软件栈里干活。

C++内存布局,对应的是算子实现时类设计和内存分配的问题;缓存命中率,对应的是数据排布和访存优化的底层问题;死锁和并发,对应的是推理引擎多流调度的问题;卷积尺寸和计算图内存复用,对应的是推理引擎图优化和内存规划的问题;算法题,对应的是基本的工程编码能力。一条线串下来,你会发现这些考点不是随意拼凑的,而是从“一个推理引擎的开发者每天要面对什么”反推出来的。

这几年寒武纪的软件栈越来越重,从最初以芯片和底层驱动为主,到现在开发者社区里能直接下载到MagicMind这样的推理加速引擎产品。MagicMind解决的问题,恰恰就是这套卷子里那几道看似“零散”的题目:如何把训练好的模型转换成一个在MLU芯片上高效执行的推理程序。这中间涉及计算图的解析与优化、算子融合、数据布局转换、内存复用、静态/动态shape处理、多流并发调度——每一环都对应着笔试里某个具体的知识点。

6.2 给后来的投递者:具体准备清单

如果你现在准备投寒武纪的软件岗笔试,我的建议是按照下面这个清单准备:

考察方向必会知识点建议资料
C/C++虚函数与内存布局、智能指针、move语义、内存对齐、模板基础《Effective Modern C++》、各种C++面试题汇总
体系结构cache line、局部性原理、循环优化、SIMD基本概念《深入理解计算机系统》第6章
操作系统死锁四条件、加锁顺序、条件变量、线程同步《操作系统导论》并发部分
深度学习推理卷积输出尺寸、参数量计算、计算图、算子融合、量化基本概念各框架推理优化文档、MagicMind开发者文档
算法树、链表、栈、队列、二分、基础动态规划LeetCode高频题、剑指Offer

这里面最容易临时抱佛脚的是深度学习和体系结构。卷积计算和cache这两块,花两个晚上就能有质的提升,但收益是实实在在的。操作系统和C++则要靠平时积累,临时背题容易翻车,因为寒武纪的题经常不是直接背概念,而是给你一个场景让你分析。

6.3 考完之后,我把这套题默写了一遍

说实话,当时和我一起笔试的同学里,好几个出来就说“感觉凉了”。结果后来有人进了面试,聊天才发现大家对这套卷子的感受完全不一样。觉得“凉”的人大多是看到一堆没见过的AI软件栈场景题就慌了,觉得简单的反而是那些把每道题都当成普通面试题来答的人。

我个人在笔试后做了一件现在想想挺有帮助的事情:当天晚上趁记忆还热,把能回忆起来的题全部默写了一遍,然后逐个去查答案、推导过程、延伸阅读。这样做的好处是,一个星期后收到面试通知时,我已经把所有笔试题都吃透了。面试官追问“你笔试里矩阵转置那道题还有没有更好的优化”时,我能直接说出用AVX指令做向量化、用cache block调优、甚至用tiling处理超大矩阵多层缓存。这些延伸全都来源于笔试后的主动复盘。

所以你如果要去考寒武纪,或者准备任何一家做AI基础设施的公司,我的建议都是:考完了不等于结束了,把每道题都当成一个知识入口,顺着它往深了挖。MagicMind的文档、开发者社区的案例、各类推理引擎的源码分析,都是很好的延伸方向。笔试只能决定你能不能过筛,但真正让你在面试里发光的,永远是笔试之后你还愿意学多少。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/6 8:11:59

核显占用高怎么办?从识别igd到系统优化三方法

一次远程帮朋友看电脑&#xff0c;他发来一张任务管理器截图&#xff0c;说是打游戏的时候核显占用一直跳&#xff0c;风扇响得厉害。他搜到了一个叫“igd”的词&#xff0c;问我能不能直接关掉。这个缩写在 Windows 环境里其实很容易造成误解&#xff1a;BIOS 里经常把集成显卡…

作者头像 李华
网站建设 2026/9/4 19:47:45

文档解析新范式:视觉语言模型直出Markdown,让RAG地基更稳

很多做 RAG&#xff08;检索增强生成&#xff09;应用的同学都有过这种体验&#xff1a;模型选型、向量库调优、提示词工程都花了大力气&#xff0c;最后线上效果却卡在了一个最不起眼的环节——文档解析。PDF 里提取出来的是一堆乱码&#xff0c;表格结构全部丢失&#xff0c;…

作者头像 李华
网站建设 2026/9/7 1:23:27

上海法国宣誓翻译去哪里办?线上线下双渠道|一文理清办理要点

办理法国留学、居留、自驾换证、房补申请等业务&#xff0c;国内中文证件必须提供法国宣誓翻译件。不少上海申请者因分不清普通翻译与宣誓翻译&#xff0c;办理无效译本导致材料被退回、耽误进度。目前线上办理是最高效省心的方式&#xff0c;本文主打合规线上渠道&#xff0c;…

作者头像 李华
网站建设 2026/9/7 1:12:18

Fooocus:3 步出图的免费离线 AI 绘图完整教程

Fooocus&#xff1a;3 步出图的免费离线 AI 绘图完整教程 【免费下载链接】Fooocus Focus on prompting and generating 项目地址: https://gitcode.com/GitHub_Trending/fo/Fooocus 想用自己电脑生成 AI 图像&#xff0c;又不想折腾环境、调参数&#xff1f;Fooocus 可…

作者头像 李华