入职考试题目

工程类

  1. 利用正则表达式,在VIM中如何通过一次替换,将下面源数据转换成目标代码?
    源数据
[Tue Jun 25 10:20:10 2024] DEV 0 BDC_CTL_REG 0: addr= 0x58001100, value = 0x80400005
[Tue Jun 25 10:20:10 2024] DEV 0 BDC_CTL_REG 1: addr= 0x58001104, value = 0x00000100
[Tue Jun 25 10:20:10 2024] DEV 0 BDC_CTL_REG 2: addr= 0x58001108, value = 0x09301c00

目标代码

int addr0=0x58001100, value0=0x80400005;
int addr1=0x58001104, value1=0x00000100;
int addr2=0x58001108, value2=0x09301c00;

答:

:%s/.*\(\d\): addr= \(0x\x\+\), value = \(0x\x\+\)/int addr\1=\2, value\1=\3;/g
  1. 在一个文件夹中,利用touch data{5..200}.txt命令生成了一批文件,如何用bash脚本,将这批文件按下面规则重命名: data5.txt重命名为000.txt, data6.txt重命名为001.txt, …, data200.txt重命名为195.txt?
    答:
# bash.sh脚本内容如下,使用./bash.sh执行

#!/bin/bash
rm *.txt
touch data{5..200}.txt

for i in {5..200}
do
   mv data$i.txt $(printf "%03d" $[i-5]).txt
done
  1. C语言和C++中让在main函数执行前运行某个函数有哪些方法?
    答:
1. 全局对象的构造函数(仅C++2. 全局变量、全局对象、静态变量、静态对象的空间分配和赋值
3. 启动进程时的初始化代码
4. __attribute__((constructor)) func() {
   //
}
  1. 请用宏尽量简化下面代码?
void conv_global_layer() {
    printf("this is conv global layer\n");
}
void pool_global_layer() {
    printf("this is pool global layer\n");
}
void transpose_global_layer() {
    printf("this is transpose global layer\n");
}

void conv_local_layer(){
    printf("this is conv local layer\n");
}
void pool_local_layer(){
    printf("this is pool local layer\n");
}
void transpose_local_layer(){
    printf("this is transpose local layer\n");
}

void conv_shape_infer(){
    printf("this is conv shape infer\n");
}

void pool_shape_infer(){
    printf("this is pool shape infer\n");
}

void transpose_shape_infer() {
    printf("this is transpose shape infer\n");
}

void conv_core() {
    printf("this is conv core\n");
}

void pool_core() {
    printf("this is pool core\n");
}

void transpose_core() {
    printf("this is transpose core\n");
}

答:

#include <stdio.h>

#define DEFINE_LAYER_FUNCTION(func_name) \
void func_name##_global_layer() { \
   printf("this is %s global layer\n", #func_name); \
} \
void func_name##_local_layer() { \
   printf("this is %s local layer\n", #func_name); \
} \
void func_name##_shape_infer() { \
   printf("this is %s shape infer\n", #func_name); \
} \
void func_name##_core() { \
   printf("this is %s core\n", #func_name); \
}

DEFINE_LAYER_FUNCTION(conv)
DEFINE_LAYER_FUNCTION(pool)
DEFINE_LAYER_FUNCTION(transpose)

int main() {
   conv_global_layer();
   pool_global_layer();
   transpose_global_layer();
   conv_local_layer();
   pool_local_layer();
   transpose_local_layer();
   conv_shape_infer();
   pool_shape_infer();
   transpose_shape_infer();
   conv_core();
   pool_core();
   transpose_core();
}
  1. 下面代码有什么问题?
std::vector<int> data={1};
auto& first = data[0];
data.push_back(2);
data.push_back(3);
data.push_back(4);
std::cout<<"first value: "<<first<<std::endl;

答:

这段代码可以正常编译运行并输出“first value: 1”,不过存在一些问题:
   1. 在这段代码中的first是对data[0]的引用,这段代码中的push_back操作会在vector后面添加元素并不会改变已经存在的元素地址,且不会引起错误并正常输出“first value: 12. 如果data的容量不足以承担push_back的操作,对于std::vector再进行push_back操作可能会导致data重新分配内存,从而导致first的引用失效,例如在“data.push_back(4);”的后面再加入“data.push_back(5);data.push_back(6);”之后,运行代码后的输出将不符合预期,并且输出的值不固定。

  1. 设计一个C函数接口,实现四维的NCHW排列的tensor数据转为NHWC?
    答:
#include <stdio.h>
#include <stdlib.h>

void nchw_to_nhwc(const float* nchw, float* nhwc, int n, int c, int h, int w) {
   for (int i = 0; i < n; i++) { // 遍历批量
       for (int j = 0; j < h; j++) { // 遍历高度
           for (int k = 0; k < w; k++) { // 遍历宽度
               for (int l = 0; l < c; l++) { // 遍历通道
                   // 计算 NCHW 和 NHWC 的索引
                   nhwc[i * h * w * c + j * w * c + k * c + l] =
                       nchw[i * c * h * w + l * h * w + j * w + k];
               }
           }
       }
   }
}
int main() {
   int n = 2; // batch size
   int c = 3; // number of channels
   int h = 2; // height
   int w = 2; // width
   // 原始 NCHW 数据
   float nchw[] = {
              // batch channel height
        1, 2, //   0       0      0
        3, 4, //   0       0      1
        5, 6, //   0       1      0
        7, 8, //   0       1      1
        9,10, //   0       2      0
       11,12, //   0       2      1
       13,14, //   1       0      0
       15,16, //   1       0      1
       17,18, //   1       1      0
       19,20, //   1       1      1
       21,22, //   1       2      0
       23,24  //   1       2      1
   };
   float* nhwc = (float*)malloc(n * h * w * c * sizeof(float));
   nchw_to_nhwc(nchw, nhwc, n, c, h, w);
   for (int i = 0; i < n; i++) {
       printf("Batch %d:\nChannel0  Channel1  Channel2\n", i);
       for (int j = 0; j < h; j++) {
           for (int k = 0; k < w; k++) {
               for (int l = 0; l < c; l++) {
                   printf("%f ", nhwc[i * h * w * c + j * w * c + k * c + l]);
               }
               printf("\n");
           }
           printf("\n");
       }
   }
   free(nhwc);
   return 0;
}
  1. C++实现一个计时机制,通过在多个被计时函数中声明该实例,自动收集其运行时间,在程序退出前,打印出所有被计时函数的函数名及执行起止时间。
    答:
#include <string>
#include <iostream>
#include <chrono>
#include <vector>

class Timer {
public:
   Timer(const std::string& functionName)
       : functionName(functionName), start(std::chrono::high_resolution_clock::now()) {
   }

   ~Timer() {
       auto end = std::chrono::high_resolution_clock::now();
       auto duration = std::chrono::duration_cast<std::chrono::microseconds>(end - start).count();
       std::cout << "function name: " << functionName << " ; execution time: " << duration << " microseconds" << std::endl;
   }

private:
   std::string functionName;
   std::chrono::high_resolution_clock::time_point start;
};

class TimerManager {
public:
   static void addTimer(const std::string& functionName) {
       timers.push_back(functionName);
   }

   static void printTimers() {
       for (const auto& timer : timers) {
           std::cout << "Function: " << timer << " was timed." << std::endl;
       }
   }

private:
   static std::vector<std::string> timers;
};

std::vector<std::string> TimerManager::timers;

#define TIMER() TimerManager::addTimer(__FUNCTION__); Timer timer(__FUNCTION__)

void function_1() {
   TIMER();
   // Simulate work
   for (volatile int i = 0; i < 1000000; ++i);
}

void function_2() {
   TIMER();
   // Simulate work
   for (volatile int i = 0; i < 5000000; ++i);
}

int main() {
   function_1();
   function_2();

   // Print all timers
   TimerManager::printTimers();

   return 0;
}
  1. 有一个二进制文件,连续存放多个TLV数据块。TLV是指类型、长度、数据,每个块第一个4字节整数为类型,第二个4字节整数为其后的数据长度,最后是对应长度的数据;类型定义为TENSOR=0, MATRIX=1, VECTOR=2。写一个Python程序,解析该文件。
    答:
import struct

# 定义类型常量
TENSOR = 0
MATRIX = 1
VECTOR = 2

def parse_tlv_file(file_path):
   with open(file_path, 'rb') as file:
       while True:
           # 读取类型和长度
           file_head = file.read(8)
           if not file_head:
               break  # 判断文件是否结束

           # 解包文件开头类型和长度
           type, length = struct.unpack('II', file_head)
           # 读取数据
           data = file.read(length)
           if len(data) < length:
               print("Expected {} bytes vs {} bytes.".format(length, len(data)))
               break
           
           # 处理数据
           if type == TENSOR:
               print("TENSOR: length {}, Contents: {}".format(length, data))
           elif type == MATRIX:
               print("MATRIX: length {}, Contents: {}".format(length, data))
           elif type == VECTOR:
               print("VECTOR: length {}, Contents: {}".format(length, data))
           else:
               print("Unknown type: {}".format(type))

if __name__ == "__main__":
   file_path = 'data_tlv.bin'  # 替换为你的文件路径
   parse_tlv_file(file_path)
  1. 有一个文件夹里面有若干个可独立编译的C++程序代码文件,共同依赖common目录里代码编译出的库,里面已经带有CMake编译脚本,可编译出正常的.so库;另外带有cv_前缀的文件依赖于opencv, 带有ff_前缀的文件依赖于ffmpeg。opencv和ffmpeg分别放在当前目录的thirdparty/opencv和thirdparty/ffmpeg, 两目录里都有include(各种头文件)、lib文件(分别有libopencv.so和libffmpeg.so)。写一个CMake编译脚本,能自动编译所有程序代码文件。
    答:
由题干可知,假设该项目结构如下:

ptah/of/project/
├── CMakeLists.txt        # 主 CMake 编译脚本
├── common/               # 公共代码目录
│   ├── CMakeLists.txt
│   └── ...
├── thirdparty/           # 第三方依赖
│   ├── opencv/           # OpenCV 目录
│   │   ├── include/
│   │   └── lib/
│   └── ffmpeg/           # FFmpeg 目录
│       ├── include/
│       └── lib/
└── src/                  # 源代码目录
   ├── cv_example.cpp
   ├── ff_example.cpp
   └── ...

ptah/of/project/CMakeLists.txt 文件即为本题解答,在项目根目录下按照常规方法编译即可:
mkdir build && cd build
cmake ..
make


ptah/of/project/CMakeLists.txt 文件内容如下:
-------------------------------------------

cmake_minimum_required(VERSION 3.10)
project(MyProject)

# 设置 C++ 标准
set(CMAKE_CXX_STANDARD 11)
set(CMAKE_CXX_STANDARD_REQUIRED ON)

# 添加 common 目录
add_subdirectory(common)

# 设置 OpenCV 和 FFmpeg 的路径
set(OpenCV_DIR "${CMAKE_CURRENT_SOURCE_DIR}/thirdparty/opencv")
set(OpenCV_LIB_DIRS "${CMAKE_CURRENT_SOURCE_DIR}/thirdparty/opencv/lib")
set(OpenCV_INCLUDE_DIRS "${CMAKE_CURRENT_SOURCE_DIR}/thirdparty/opencv/include")
set(FFmpeg_DIR "${CMAKE_CURRENT_SOURCE_DIR}/thirdparty/ffmpeg")
set(FFmpeg_LIB_DIRS "${CMAKE_CURRENT_SOURCE_DIR}/thirdparty/ffmpeg/lib")
set(FFmpeg_INCLUDE_DIRS "${CMAKE_CURRENT_SOURCE_DIR}/thirdparty/ffmpeg/include")

# 查找 OpenCV
link_directories(${OpenCV_LIB_DIRS})
include_directories(${OpenCV_INCLUDE_DIRS})

# 查找 FFmpeg
link_directories(${FFmpeg_LIB_DIRS})
include_directories(${FFmpeg_INCLUDE_DIRS})

# 自动查找所有源文件
file(GLOB_RECURSE SOURCES "src/*.cpp")

# 遍历源文件并创建可执行文件
foreach(source_file ${SOURCES})
   get_filename_component(executable_name ${source_file} NAME_WE)
   
   # 根据文件前缀添加依赖
   if(source_file MATCHES "src/cv_.*\\.cpp$")
       add_executable(${executable_name} ${source_file})
       target_link_libraries(${executable_name} ${OpenCV_LIB} common)
   elseif(source_file MATCHES "src/ff_.*\\.cpp$")
       add_executable(${executable_name} ${source_file})
       target_link_libraries(${executable_name} ${FFmpeg_LIB} common)
   else()
       add_executable(${executable_name} ${source_file})
       target_link_libraries(${executable_name} common)
   endif()
endforeach()

-------------------------------------------
  1. 有一个工程使用git管理,依赖于其他工程代码,什么时候以submodule管理,什么时候以预编译的库文件放到工程中管理?分别说明这两种管理方法的适用场景?
    答:
   1. 使用 Git submodule:
优点缺点:
   a. 代码集中管理,便于版本控制,并且可以通过 Git 命令轻松更新到依赖库的最新版本。
   b. 对新手不友好,并且在工作过程中需要额外的步骤拉取或更新submodule。
适用场景:
   a. 在大型工程中,依赖的代码频繁更新,需要及时获取最新版本时;
   b. 使用者因为自己的开发需求,需要自行修改依赖代码时;
   c. 多个项目都依赖同一个工程代码时,submodule可以保证这些项目依赖的代码相同;
   d. 团队项目中,有多个成员需要修改依赖代码时;
   e. 工程中依赖的代码不是必须的,每个成员可以根据需求选择是否拉取。

   2. 使用预编译的库文件:
优点缺点:
   a. 使用方便,直接引用库文件即可,节省编译时间。
   b. 更新不便且版本管理困难,且使用者无法根据自己的需要进行修改。
适用场景:
   a. 依赖代码稳定,并且更新频率低时,使用预编译库可以简化项目;
   b. 编译依赖代码的时间较长时,使用预编译库可以加快使用者的编译速度;
   c. 依赖代码的编译环境或配置要求严格时,使用预编译库可以保证一致。
  1. GDB 调试程序时,发现源码不在当前机器上,如何在不退出gdb情况下,加载源进来?
    答:
   1. 将源码复制到当前机器上,然后使用directory(dir)命令添加路径:
(gdb) directory /path/to/source
   2. 使用set substitute-path替换路径:
(gdb) set substitute-path /real/source/path /fake/source/path
  1. 工程C是底层库,工程A, B分别调用C来执行特定功能。B工程在运行时,发现了一个bug,可以通过改B或改C来修复该bug,该如何选择修复?如果A要增加新功能,需要对C库中和B共用的一个接口增加一个参数,A, B, C库的代码该如何提交?
    答:
   1. 修复bug:
   a. 如果这个bug只影响B工程,并且修复难度低,可以直接在B工程中修复;
   b. 如果修复C工程可以提高代码可扩展性或减少未来产生bug的可能,并且修复C工程代码不影响其他工程调用C工程,修改C工程代码更好;
   c. 如果修改C工程会影响A工程对其的调用,则应该修改B工程。
   2. 修改代码 & 提交代码:
   修改代码:
   a. 先修改C工程代码,在目标接口增加新参数,确保修改后该接口能够实现新功能;
   b. 在B工程代码接口处增加新参数,以适配C的修改,确保C工程代码的修改不会影响B工程的运行;
   c. 在A工程代码接口处增加新参数,并且在A中增加新功能,确保该功能符合预期正确运行,并且进行测试。
   提交代码(假设使用git管理):
   a. 先提交C工程修改到远程仓库,并标明修改信息;
   b. 在修改后的B工程中,拉取远程仓库最新的C工程代码,并测试能C的修改是否会影响B的运行,确保正确后提交B工程代码到远程仓库;
   c. 在以上两步完成后,在A工程中拉取远程仓库最新的C工程代码,并且在本地对A工程进行修改以实现新功能,在新功能测试无误后,提交A工程代码的修改到远程仓库。

原理类

  1. 画图说明CPU的超标量、流水线执行指令的时序。
    答:
   1. 流水线执行:
   流水线将指令的执行分为多个阶段,如取指(IF)、译码(ID)、执行(EX),每个阶段可以在不同的时钟周期内处理,指令按照顺序执行。

   2. 超标量执行:
   超标量 CPU 能够在一个时钟周期内发射多条指令,每条指令可以在不同的执行单元中同时执行,支持指令乱序执行。
  1. 哪些地方用到了Cache,请举例说明。Cache一致性能难点在哪里?
    答:
   1. Cache 的应用场景:
   Cache 是一种利用数据局部性而设计的小存储单元,用于提高CPU数据访问速度的缓存存储器,广泛应用于计算机CPU中。现代 CPU 通常具有多级缓存(如 L1, L2 和 L3),用于存储频繁访问的数据和指令。L1 Cache 通常是最快的,容量较小,L2 和 L3 Cache 则较大但速度稍慢于 L1。并且 CPU 读取变量时,会先尝试从CPU的寄存器读取,如果寄存器中没有,则会从 L1 开始在 Cache 中读取,如果 L1 中没有该变量,则会继续尝试从 L2, L3, ... 中读取,如果所有 Cache 中都没有该变量,则会从 CPU 的外部 DRAM 中读取。

   2. Cache 一致性性能难点
   在现代的计算机系统中,如果核数不太多,一般每个核都会有本地的 L1 和 L2 Cache ,并且在多核之间共享一个 L3 Cache ,以提升CPU核的整体性能。但是如果有多个 CPU 同时访问并尝试修改同一个变量,则可能会产生多个 CPU 中同一个变量的不同本地副本不一致,这就是 CPU 的一致性难点。
  1. 请简要举例说明PCIe、DDR、GDDR、HBM的技术应用场景?
    答:
 1. PCIe(Peripheral Component Interconnect Express)
   PCIe 是一种高速串行计算机扩展总线标准,速度快并且可以较大功率供电,用于连接主板上的各种硬件,如 CPU 、显卡、硬盘、网卡等。
 2. DDR(Double Data Rate Synchronous Dynamic Random Access Memory)
   即双倍速率同步动态随机存储器,其中“双倍速率”指的是在一个时钟周期内可以传输两次数据,被广泛应用于服务器、个人计算机、数据中心、手机等设备中。
 3. GDDR(Graphics Double Data Rate Synchronous Dynamic Random Access Memory)
   即图形双倍速率同步动态随机存储器,就像它名字中的 Graphics 所描述的,这是专门为图形处理器设计的存储设备,主要用于显卡(GPU)、图形工作站等。
 4. HBM(High Bandwidth Memory)
   即高带宽内存,也是在显卡中广泛使用的存储设备。区别于 GDDR,HBM 采用垂直堆叠半导体工艺生产,可以实现低功耗、高带宽通信通道,减少了通信成本。但是由于加工成本更高,所以 HBM 更多地用于高性能计算、深度学习、高端图形处理等应用场景。
  1. 并行计算加速比如何计算?
    答:
并行计算的加速比(Speedup)是衡量并行计算相对于串行计算性能提升的一个重要指标。加速比的计算公式如下:

Speedup = T_s / T_p

其中:T_s 是任务在单个处理器下的运行时间,T_p 是在多个处理器下并行计算的运行时间。
  1. 矩阵切分时考虑的因素有哪些?
    答:
在进行矩阵切分时,通常需要考虑以下几个因素:
   1. 被切分矩阵的维度;
   2. 进行矩阵切分的目的,比如为了并行计算、优化存储、数据处理等;
   3. 矩阵的切分方式;
   4. 矩阵数据是否稀疏,如果是稀疏矩阵,则可以通过矩阵分块,来适当减小存储和计算成本;
   5. 切分后矩阵的大小;
   6. 切分后的不同矩阵,在后续计算中需要的计算时间是否均衡;
   7. 切分后的矩阵在内存中存储和访问方式;
   8. 对矩阵的切分操作是否和算法相匹配。
  1. 说一下Ring AllReduce计算流程,可以画示意图来表示,通过公式推导在N个节点组成的Ring,每个节点数据量为M字节,且带宽为B字节/秒,推导 AllReduce执行时间,如果有两个节点间的带宽是B/2, 执行时间如何变化。
    答:
Ring-Allreduce是一种以环状拓扑为基础的通信系统。整个体系结构的工作过程见图1,Rank代表了各个 GPU的进程编号,并且梯度信息可以在两个不同的区域中同步传输。在Ring-Allreduce体系结构中,每台计算机都是一个工作节点,按环形排列。Ring-Allreduce 的过程被分成两个阶段,即 Scatter-Reduce 和 Allgather。
   1. Scatter-Reduce阶段
   如图2,第(1)步,每个结点把本设备上的数据分成 N个区块, N是Ring-Allreduce体系结构中的工作节点数目。第(2)步,在第一次传输和接收结束之后,在每一个结点上累加了其他节点一个块的数据。这样的数据传输模式直到“Scatter-Reduce”阶段结束。最后每一个节点上都有一个包含局部最后结果的区块,第(3)步,深色区块表示,这个区块是所有节点相应的位置区块之和。
   2. Allgather 阶段
   Allgather阶段总共包含有数据发送和接收N一1次,不同的是,Allgather阶段并不需要将接收到的值进行累加,而是直接使用接收到的块内数值去替环原来块中的数值。在迭代完第1次这个过程后,每个节点的最终结果的块变为2个,如图3步骤(2)所示。之后会继续这个迭代过程直到结束,使得每一个节点都包含了全部块数据结果。下图为整个Allgather过程,可以从图中看到所有数据传输过程和中间结果值。


对于带宽为B字节/秒,在不考虑计算耗时的情况下,分别计算scatter-reduce和allgather阶段的执行时间。
T_scatter   = ( M / B ) * ( N - 1 )
T_allgather = ( M / B ) * ( N - 1 )
T_total     = ( M / B ) * ( N - 1 ) * 2
如果其中两个节点间的带宽为 B/2,其他节点间的带宽仍为 B,则总执行时间会变为原来的两倍。因为数据传输的时间取决于链条中最慢的速度。
  1. AI编译器与传统的编译器有什么区别?说几个两者都用到的优化手段?
    答:
   1. 区别:
   a. 设计目标不同
       传统编译器主要关注代码的性能优化,例如循环展开、常量折叠等,关注内存、CPU等,主要针对通用硬件,例如 CPU, GPU。而 AI 编译器更偏重于机器学习模型的优化,例如模型的量化、剪枝等,并且可能针对特定的 AI 相关硬件进行针对性优化以提高模型运行效率,一般是针对专用的 AI 硬件,例如 TPU 或 NPU,我们平时使用的 TPU-MLIR 就是一种 AI 编译器。

   b. 输入和输出不同
       传统编译器的输入是源代码,例如 *.c, *.cpp 等源代码文件,输出为 CPU 机器代码等。AI 编译器的输入一般是机器学习模型,例如 Pytorch, TensorFlow, ONNX 模型,输出为优化后的模型,一般是针对特定硬件的代码。

   c. 适应性
       传统编译器,一般使用静态分析技术进行优化,可能会存在很多阶段,并且产生一些中间文件,优化会在编译时确定,无法调整。AI 编译器利用图优化技术来优化模型的计算图,具有更强的适应性,可以根据运行时的特征进行动态优化,还可以使用机器学习技术来学习和适应新的优化策略。

   2. 共同使用的优化手段:

   a. 循环优化: 减少循环的迭代次数以提高运行速度,将大循环分成小块以提高缓存利用率。

   b. 内存管理优化: 通过分析内存访问模式来减少缓存,或者优化数据结构的布局,以提高数据的局部性。

   c. 并行化: 将计算任务分解为可并行执行的小任务,或者将数据分发到多个计算单元来实现并行处理。

  1. 模型训练和推理对加速器的要求有什么区别?
    答:
模型训练和推理是两个不同的场景,是对加速器的要求有着明显区别
   1. 计算量需求
   模型训练通常涉及大量的矩阵运算,会消耗大量的计算资源。通常使用能够并行处理大量数据的硬件来加速训练过程,例如GPU或TPU。模型推理相对计算需求较低,对延迟要求高,通常需要快速响应,在某些场景中专用的推理芯片更受欢迎。
   2. 内存带宽需求
   模型训练需要高带宽来处理大规模的数据集和模型参数,大型模型也需要更多的显存来存储中间激活值和计算出来的梯度。模型推理只需处理输入数据和输出结果,对内存带宽的需求相对较低。
   3. 并行性
   模型训练需要进行大规模并行计算,通常使用多GPU或分布式训练。而模型推理可能需要支持并发推理,以处理多个请求,但并行性需求相对较低。
   4. 能耗
   虽然模型训练的能耗较大,但是现阶段的模型训练中,能效比并不是主要关注点。相反,模型推理的能效比非常重要,尤其是在边缘设备或大规模部署中,低功耗是非常重要的考量指标。
  1. Pooling和depthwise conv在什么时候可以等价?
    答:
在 Depthwise Conv 的卷积核权重均为 1
  1. 2D卷积与矩阵乘法在何种情况下可以等价?能否通过数据排列使二者转换?
    答:
可以通过数据排列的方法将2D卷积操作转换为矩阵乘法操作,在特定的情况下可以等价。
将图像中的每个卷积窗口展开成列,每列的元素数量为卷积核元素数量,然后将不同的列排在一起组成一个大矩阵。然后将卷积核排列为一个行向量,然后将这个行向量作为左矩阵,将图像矩阵重排后的矩阵作为右矩阵,进行矩阵乘运算后可得到一个行向量。再将得到的行向量的元素进行重新排列即可得到结果。

例如,我们有一个 4 * 4 的图像和一个 2 * 2 的卷积核,我们可知卷积窗口为 9 个,每个卷积窗口的大小就是 2 * 2 ,将每个卷积窗口展开成列组成一个 4 * 9 的大矩阵。再将卷积核的元素排列成一个 1 * 4 的行向量。将卷积核重排得到的 1 * 4 行向量与图像展开后得到的 4 * 9 大矩阵进行矩阵乘,即可得到一个 1 * 9 的行向量,将其重新排列成 3 * 3 的矩阵即可得到2D卷积结果。

核心类

  1. TPU中有哪几种数据存储模块,并且在什么情况下使用?
    答:
TPU中的存储模块有 LMEM, SMEM, GMEM, L2M,其中使用场景如下:
   1. LMEM(local memory)
   用于存储TPU计算单元所需的数据和计算结果
   2. SMEM(static memory)
   用于存储常数数据,如泰勒展开、查表表项、常数数列等,部分TPU指令可以访问SMEM并将数据加载到LMEM中。
   3. GMEM(global memory)
   是TPU上的DRAM内存资源,用于存储指令、输入输出数据、计算结果等。
   4. L2M(level-2 sram)
   用于TPU中不同core间的数据共享、中间数据缓存等。
  1. TPU中多个engine,他们有哪几种同步机制,分别适用哪种情况。选一种同步机制画图表示出来?
    答:
芯片内Engine间,有消息同步方式、 sync_id 两种、msg sync机制,
   1. sync_id
   在各分组中的GDMA和TPU间可以采用此方式进行同步。GDMA和TPU指令中有depend id的字段,表示需要对方Engine执行到此ID时,己方Engine才能继续执行下去;若depend id为0,表示己方Engine可以立即执行不用等待。
   2. 消息同步
   消息同步方式是以消息为载体,需要同步的Engine间通过发送和等待消息的形式来实现通讯和同步的目的。
   3. msg sync
   参与同步的engine会往自身指令队列中push一条sendMsg和waitMsg。
  1. 哪些指令对数据排列有特殊要求(为什么)?
    答:
因为 TPU 本身的架构是针对并行计算和tensor数据进行设计的,为了内存访问效率和计算性能,可能需要规定数据存储的方式,以提高计算过程中数据的读取速度,所以很多指令对数据排列都有要求,具体的指令信息和指令约束可以参考指令集设计文档,这里举几个例子:
   1. Convolution(CONV): 要求输入矩阵为 NCHW 对齐存储或 Tensor 存储;
   2. Matrix_multiply(MM), Matrix_multiply2 (MM2): 要求对齐存储;
   3. Pooling/Depthwise (PorD): 要求输入 tensor 对齐存储、Kernel tensor 紧密存储;
   4. Fused_compare (CMP): tensor A 和 tensor B 对齐存储或 Stride(0,0,0,0) 存储,tensor C 和 tensor D 对齐存储;
   5. Special_function (SFU): 除泰勒展开系数表外,所有操作数和结果在LMEM中对齐存储;泰勒展开系数表为一维标量,存储在LMEM中(泰勒展开表a0-an顺序存放),其实地址按16字节对齐;所有操作数不要求start_lane=0, 但是start_lane须相同。
  1. tpu_poll与tpu_sync_all()函数有什么区别?
    答:
tpu_poll() 函数用于检查 TPU 上的某个操作是否完成,它通常用于异步操作,以确定某个任务是否已经结束,并且这个函数不会将整个程序暂停。
tpu_sync_all() 函数用于同步所有 TPU 核心,用于确认先前发射到 TPU 的指令都已完成,并且这个函数会在接收到所有指令完成的命令前暂停程序的运行,直到所有 TPU 指令都得到执行。
  1. TpuDNN使用时,涉及到哪些动态库?画出TpuDNN下,Host、驱动、TPU交互的时序图
    答:
TPU-dnn三基于runtime编写的算子推理库,需要用到 bmlib(libsophon), firmware_base, firmware_core, tpu-kernel 等库。
  1. 多核TPU算子开发中,有哪些优化手段?
    答:
   1. 进行并行计算
   2. 使用分布式模型
   3. 内存优化,减少带宽使用,提高内存访问效率
   4. 合理使用异步执行和流水线技术,最大化利用计算单元,减少等待时间
   5. 对模型进行量化和稀疏化处理等
   6. 对类似卷积等操作使用一些具有针对性的优化手段
  1. PIO模式如何转换成Descriptor模式运行,说说大致思路,可能有哪些难点?在什么情况下不能等价运行?
    答:
模式介绍:
   PIO模式:
       是一种传统的I/O操作模式,通过‌CPU执行I/O端口指令来进行数据的读写的数据交换模式。在这种模式下,CPU直接参与数据传输的每一个步骤,通过读取和写入I/O端口来与外设进行通信
   DESC模式:
       是一种先进的I/O操作模式,使用描述符(Descriptor)来管理数据传输。描述符是一种数据结构,包含了数据缓冲区的地址、长度和其他控制信息。DMA(Direct Memory Access)控制器通常用于处理描述符,从而减轻CPU的负担。

将PIO模式转换为Descriptor模式大致思路:
   1. 因为Descriptor模式需要一些数据缓冲区,所以通常需要为其配置另外的寄存器以满足要求;
   2. 有了专用寄存器,还需要设计一个配套的缓冲区管理机制,并建立一套软件系统来管理设备的状态,以确保数据传输过程中不会出现错传、漏传等;
   3. 将 PIO 模式下的各个步骤用描述符表示,并分析得到时序、地址、数据长度等信息;
   4. 将描述符发送到设计好的 Descriptor 模式管理程序。

可能的难点:
   1. Descriptor模式相比PIO模式更加复杂和精巧,通常会涉及到中断处理、访问忙碌设备等,所以需要更加复杂的管理模块,来分配带宽、存储等;
   2. 并且可能会由此产生一些软件层面的优化工作,以达到预期的性能提升。并且不是所有的硬件都支持Descriptor模式,也不是所有硬件都能通过 Descriptor 模式得到提升;
   3. PIO 模式下 CPU 可以精确控制每一步操作的时序,这在 Descriptor 模式下还需要专门进行处理;
   4. PIO 模式下 CPU 可以立即检测和处理传输错误,而在 Descriptor 模式下存在困难。

不能等价运行的情况:
   1. 排除硬件不支持导致的不等价情形,如果数据量较小或数据shape固定,使用PIO模式可能更高效。
   2. 或者某些特定的I/O操作无法通过 Descriptor 模式实现,例如一些需要对时序要求非常高的算法。

  1. CModel有指令保存功能,是如何实现的,有哪些用途?
    答:
Cmodel是模拟 TPU 硬件运行的软件代码,可以模拟设备内存、各个单元、指令执行、多核并行等。这就涉及到通过host端发送指令到device端,然后device端执行指令,所以在运行过程中会有另外申请的内存空间,专门用来保存指令信息,也就起到了保存静态指令的功能。

在研发过程中,可以有很多用途:
   1. 可以使用保存下来的指令信息来识别和定位错误信息,并且可以配合板卡使用,调用Cmodel保存指令buffer来记录硬件的指令和行为;
   2. 通过保存的指令,可以了解系统在不同条件下的功能是否正确,来进行系统验证;
   3. 分析性能。

  1. 在算子执行过程中,Sync ID是怎么管理维护的,来达到engine并行执行的要求的?
    答:
   1. 每个算子在提交给计算引擎(engine)时,会被分配一个唯一的Sync ID。 Sync ID 由全局计数器生成,确保每个算子都有一个唯一的标识符;
   2. Sync ID 可以用来记录算子是否执行完成,在算子执行前,会检查该算子依赖的算子 Sync ID ,来确保该算子能够被正确执行;
   3. 在算子执行完成后,更新 Sync ID 来记录算子已经执行完成;
   4. 在并行计算中,也可以通过 Sync ID 的状态来同步每个 engine;
   5. 芯片中负责调度的部分会根据 Sync ID 的状态来分配或发射指令。
   6. 在芯片内部,Sync ID 是通过 Sync Agent 和 Sync Hub 来管理的。 Sync Agent 存在于各个硬件模块中,用于维护自身所在模块及相关模块的 SYNC ID,每个 Agent 通过一对 8-bit TBUS与 Sync Hub 连接。 Sync Hub 提供最多16对 TBUS连接 Sync Agent,当某个 Agent 发送了更新的 SYNC ID 到 Sync Hub 后,它会将其广播给其他 Agent。
  1. 给定一个模型或算法,如何在TPU上粗略估计最短运行时间?
    答:
先分析模型或算法的计算复杂性,如:参数总量、模型计算图、主要操作类型(如卷积、矩阵乘)、操作数等。然后根据 TPU 的具体架构和性能参数,结合计算复杂度的估计,大致估计出需要的运算次数及利用率,用运算次数除以运算频率,然后考虑带宽、模型加载时间、批处理大小等因素,即可估计出最短运行时间。当然,对于一些与测试过的模型相似的模型或算法,我们只需要查询测试数据,再根据模型大小等因素即可估算出运行时间。
  1. TPU的算力和带宽越大越好吗?还要考虑哪些因素?
    答:
在不考虑其他因素且芯片其他性能固定的情况下,算力和带宽当然是越大越好,但是在实际的芯片设计过程中,影响实际运行速度和运行时间的因素有很多。例如:内存访问模式、运行的模型或算法是否可以充分利用 TPU 的全部算力、算法并行度、数据存储形式、数据集大小等。由于工艺和基础理论的影响,芯片的面积是非常宝贵的,需要平衡考虑其上的各个部分所占的面积及可能的利用率。如果只追求算力或带宽而忽略了数据存储模式、通信机制、同步机制带来的性能提升以及带来的成本上升,是非常不划算的。
  1. 适合用硬件加速的算法具有什么特点,列举至少两个?
    答:
适合用硬件加速的算法通常具有高度并行性和计算密集型这两个主要特点。这些特点使得算法能够充分利用硬件加速器的并行计算能力和高算力,从而显著提升性能。

适合用硬件加速的算法,通常具有以下特点:
   1. 高并行度
   在神经网络中,可以将图像拆分成多个小单元分别处理,这样可以充分利用 TPU 或 GPU 等硬件设备的并行能力来加速计算,或者将矩阵的乘法操作分在多个处理单元上同时进行;
   2. 数据吞吐量大
   算法需要处理大量数据,能够通过设计针对的存储器件对其进行加速;
   3. 数据局部性
   算法中具有大量的局部性变量或数据,可以针对这个性质设计专用存储,减少数据在各个单元、各个核之间的传输,从而达到加速的目的;
   4. 计算模式相对固定
   某些算法的计算步骤相对固定,适合在专用的硬件上处理;
   5. 可重复性高
   某些算法的运行时间主要消耗在为数不多的几个运算中,可以将其写在芯片上以达到加速目的。
  1. TPU上sin算子的计算流程是怎样的?怎么能提升其计算精度?
    答:
   1. 在计算机中计算一个给定的 x 的正弦值 sin(x) ,通常有以下流程:
   a. 输入准备:利用弧度的周期性,将输入值 x 通过公式变换到(−π,π]范围内以减少计算复杂度,然后将数据存储到 TPU 的存储单元。 
   b. 计算正弦值:对于一些常见的输入值, TPU 可以通过查表来快速获取对应的 sin 值;对于不再表中的输入值,需要利用泰勒展开、多项式逼近等适合计算机的数学方法进行计算,可以在 TPU 的计算单元上并行执行。
   c. 后处理:例如恢复原始输入值的范围,然后将输出值输出到外部存储。

   2. 提高计算精度的方法
   a. 扩大查找表的范围,存入更多输入值对应的结果,但是这样做会占用芯片空间,对于包含大量 sin 算子的模型较为合适;
   b. 提高计算过程中的数据精度,尽量避免舍入误差;
   c. 增加泰勒展开的级数,也就是增加泰勒展开的高阶项,来提高计算精度,但是这样做显然会增加计算量;
   d. 对于一些与查找表的数值相近的输入值,可以综合使用查表法和泰勒展开法,先在表中获得近似值,再通过在该值处的泰勒展开,计算得到精确值。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值