Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 7 additions & 2 deletions Makefile
Original file line number Diff line number Diff line change
@@ -1,15 +1,20 @@
.PHONY : build clean format install-python test-cpp test-onnx
.PHONY : build clean format install-python test-cpp test-onnx submodules

TYPE ?= Release
TEST ?= ON

CMAKE_OPT = -DCMAKE_BUILD_TYPE=$(TYPE)
CMAKE_OPT += -DBUILD_TEST=$(TEST)

build:
# 注意:build 必须是第一个目标,否则 `make` 的默认目标会变成 submodules
build: submodules
mkdir -p build/$(TYPE)
cd build/$(TYPE) && cmake $(CMAKE_OPT) ../.. && make -j8

# 初始化 git submodule(googletest 等第三方依赖,clone 后首次构建必须执行)
submodules:
git submodule update --init --recursive

clean:
rm -rf build

Expand Down
1,026 changes: 1,026 additions & 0 deletions docs/作业完成说明.md

Large diffs are not rendered by default.

Binary file added docs/通过截图.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
6 changes: 5 additions & 1 deletion docs/项目部署.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,9 +27,13 @@ sudo apt install make
# mac 使用Homebrew安装
brew install make
```

### 构建命令

配置好上述环境后,进入项目目录后可以通过以下命令进行构建。
> 注意:`3rd-party/googletest` 是 git submodule,`make build` 会自动执行
> `git submodule update --init --recursive` 初始化;若网络不佳导致子模块
> 拉取失败,可手动执行该命令后重试。

- `make`/`make build`: 构建整个项目;
- `make test-cpp`: 构建项目后执行测例;
- `make clean`:清理生成文件
9 changes: 8 additions & 1 deletion include/core/allocator.h
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,14 @@ namespace infini {
// TODO:可能需要设计一个数据结构来存储free block,以便于管理和合并
// HINT: 可以使用一个 map 来存储 free block,key 为 block 的起始/结尾地址,value 为 block 的大小
// =================================== 作业 ===================================

// 踩坑(详见 docs/作业完成说明.md 作业一):
// - alloc/free 只是"模拟"分配(函数开头有 IT_ASSERT(ptr == nullptr)),
// 真实内存必须在全部规划完成之后通过 getPtr() 一次性分配;
// - alloc 需要支持"末尾空闲块扩展"(地址最大的空闲块延伸到 peak 时,
// 可以从该块起始地址继续向外扩展),否则 test_allocator 的
// testAllocWithEndFreeBlock 无法通过;
// - free 时记得与前一块、后一块做合并(先向前、再向后),避免碎片。
std::map<size_t, size_t> freeBlocks;
public:
Allocator(Runtime runtime);

Expand Down
58 changes: 57 additions & 1 deletion src/core/allocator.cc
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
#include "core/allocator.h"
#include <algorithm>
#include <cstddef>
#include <iterator>
#include <utility>

namespace infini
Expand Down Expand Up @@ -32,8 +35,42 @@ namespace infini
// =================================== 作业 ===================================
// TODO: 设计一个算法来分配内存,返回起始地址偏移量
// =================================== 作业 ===================================
// - 推荐 first-fit:从低地址开始扫描 free list,找到第一个足够大的块;
// - 块比需要的大时,把剩余部分切回 free list;
// - 找不到合适块时,在 peak 处追加;若地址最大的空闲块恰好延伸到
// peak(first + second == peak),从它的起始地址向外扩展(见头文件踩坑);
// - 分配成功记得更新 used、peak(peak = max(peak, used))。
for(auto& it : freeBlocks) {
if(it.second >= size) {
size_t addr = it.first;
if(it.second > size) {
freeBlocks[addr + size] = it.second - size;
}
freeBlocks.erase(it.first);
used += size;
peak = std::max(peak, used);
return addr;
}
}
// 如果地址最大的空闲块紧贴着内存末尾
// 为了复用内存,就从该内存块开始向后拓展
if(freeBlocks.empty() != true) {
auto last = std::prev(freeBlocks.end());
if(last->first + last->second == peak) {
last->second += size;
size_t addr = last->first;
used += size;
peak = addr + size;
freeBlocks.erase(last);
return addr;
}
}

size_t addr = peak;
used += size;
peak += size;

return 0;
return addr;
}

void Allocator::free(size_t addr, size_t size)
Expand All @@ -44,6 +81,25 @@ namespace infini
// =================================== 作业 ===================================
// TODO: 设计一个算法来回收内存
// =================================== 作业 ===================================
// - 把 [addr, addr + size) 放回 free list(used 相应减少);
// - 先与前一块合并(prev->first + prev->second == addr),
// 再与后一块合并(it->first + it->second == next->first),顺序不能反。
auto it = freeBlocks.emplace(addr, size).first;
used -= size;

if(it != freeBlocks.begin()) {
auto prev = std::prev(it);
if(prev->first + prev->second == addr) {
prev->second += size;
freeBlocks.erase(it);
it = prev;
}
}
auto next = std::next(it);
if(next != freeBlocks.end() && it->first + it->second == next->first) {
it->second += next->second;
freeBlocks.erase(next);
}
}

void *Allocator::getPtr()
Expand Down
190 changes: 190 additions & 0 deletions src/core/graph.cc
Original file line number Diff line number Diff line change
@@ -1,7 +1,17 @@
#include "core/graph.h"
#include "core/blob.h"
#include "core/ref.h"
#include "core/runtime.h"
#include "operators/matmul.h"
#include "operators/transpose.h"
#include <algorithm>
#include <cstddef>
#include <iterator>
#include <map>
#include <numeric>
#include <queue>
#include <unordered_set>
#include <vector>

namespace infini
{
Expand Down Expand Up @@ -106,6 +116,136 @@ namespace infini
// 1. 去除冗余的算子(例如,两个相邻的算子都是 transpose 算子,且做的是相反的操作,可以将其全部删除)
// 2. 合并算子(例如,矩阵乘算子中含有属性transA、transB,如果其输入存在transpose,且对最后两个维度做交换,就可以将transpose融入到矩阵乘算子的属性中去)
// =================================== 作业 ===================================
// 踩坑(详见 docs/作业完成说明.md 作业八):
// - 执行顺序:必须先做规则 1(消除互逆 transpose 对),再做规则 2
// (融合进 matmul),否则本应被抵消的 transpose 会被错误融合成 transA;
// - 删除算子/重连输入时,必须同步维护双向关系(tensor 的 source/targets
// 与 op 的 predecessors/successors),否则会残留失效的 weak_ptr,
// 打印图时崩溃(段错误 / bad_weak_ptr);
// - 两个 permute 互逆 ⇔ 复合为恒等置换:p2[p1[i]] == i;
// - 只交换最后两维 ⇔ 前 rank-2 维不动,最后两维互换;
// - 删除中间 tensor 前检查它是否还被其他算子消费(targets 非空则保留)。
auto isInverse = [](const std::vector<int> &p1, const std::vector<int> &p2) {
if(p1.size() != p2.size()) {
return false;
}
for(size_t i = 0; i < p1.size(); i++) {
if(p2[(size_t)p1[i]] != (int)i) {
return false;
}
}
return true;
};
auto isSwapLastTwo = [](const std::vector<int> &perm) {
size_t r = perm.size();
if (r < 2)
return false;
for (size_t i = 0; i < r; ++i)
{
int expect = (int)i; // 默认保持原位
if (i == r - 2) // 倒数第二维映射到最后一位
expect = (int)(r - 1);
else if (i == r - 1) // 最后一位映射到倒数第二维
expect = (int)(r - 2);
if (perm[i] != expect)
return false;
}
return true;
};
// ==================== 规则 1:去除冗余的互逆 transpose 对 ====================
bool changed = true;
while (changed){
changed = false;
for (auto &op : ops){
if (op->getOpType() != OpType::Transpose) {
continue;
}
auto trans = as<TransposeObj>(op); // mid -> out
auto mid = trans->getInputs(0);
auto pred = mid->getSource(); // input -> mid 获取mid的生产者
if (!pred || pred->getOpType() != OpType::Transpose){
continue;
}
// 安全条件:mid 只被 trans 这一个算子消费,否则不能删除 pred
if (mid->getTargets().size() != 1) {
continue;
}
// 如果两个permute不互为逆置换,continue
if (!isInverse(as<TransposeObj>(pred)->getPermute(),
trans->getPermute())) {
continue;
}

auto input = pred->getInputs(0);
auto out = trans->getOutput(0);
// 先记住 out 是否有消费者(决定它是否还能删除)
bool outHasConsumers = !out->getTargets().empty();
// 把 out 的所有消费者(后继算子)的输入从 out 换成 input
for (auto &succ : out->getTargets()) {
succ->replaceInput(out, input);
out->removeTarget(succ);
input->addTarget(succ);
succ->removePredecessors(trans);
trans->removeSuccessors(succ);
}
input->removeTarget(pred);
mid->removeTarget(trans);
trans->removePredecessors(pred);
pred->removeSuccessors(trans);

removeOperator(trans);
removeOperator(pred);
removeTensor(mid);

// out 原本有消费者且已全部改接到 input,可以一并删除;
// 若 out 是图的输出(本来就没有消费者),则予以保留。
if (outHasConsumers) {
removeTensor(out);
}
changed = true;
break; // 重新扫描
}
}
// ============ 规则 2:把输入上的 transpose 融合进 matmul 的 trans 属性 ============
for (auto &op : ops)
{
if (op->getOpType() != OpType::MatMul) {
continue;
}
auto matmul = as<MatmulObj>(op);
for (int i = 0; i < 2; ++i)
{
auto in = matmul->getInputs(i);
auto src = in->getSource();
if (!src || src->getOpType() != OpType::Transpose) {
continue;
}
// 安全条件:该 transpose 只被 matmul 一个算子消费
if (in->getTargets().size() != 1) {
continue;
}
if (!isSwapLastTwo(as<TransposeObj>(src)->getPermute())) {
continue;
}
auto trans = as<TransposeObj>(src);
auto newIn = trans->getInputs(0);
if (i == 0) {
matmul->setTransA(true);
}
else{
matmul->setTransB(true);
}
matmul->replaceInput(in, newIn);
in->removeTarget(matmul);
newIn->addTarget(matmul);
newIn->removeTarget(trans);
matmul->removePredecessors(trans);
trans->removeSuccessors(matmul);
in->setSource(nullptr);
removeOperator(trans);
removeTensor(in);
}
}
}

Tensor GraphObj::getTensor(int fuid) const
Expand Down Expand Up @@ -152,7 +292,57 @@ namespace infini
// TODO:利用 allocator 给计算图分配内存
// HINT: 获取分配好的内存指针后,可以调用 tensor 的 setDataBlob 函数给 tensor 绑定内存
// =================================== 作业 ===================================
// 踩坑(详见 docs/作业完成说明.md 作业一 step3):
// - 必须分两阶段:先对每个 tensor 做"模拟"分配(alloc/free,只算偏移量),
// 最后统一调用 allocator.getPtr() 拿真实内存基址再绑定 Blob。
// 不能边 alloc 边 getPtr()(alloc 开头有 IT_ASSERT(ptr == nullptr));
// - 建议用引用计数做内存复用:记录每个 tensor 剩余消费者个数(targets 数),
// 某个 op 规划完后其输入计数减一,归零即 free,供后面算子复用;
// - 每个 tensor 只分配一次(用一个集合记录已分配的 tensor,避免重复分配);
// - 重要:算子的"输出"必须先于"输入回收"来分配,
// 否则输出会复用本算子输入的内存,而 kernel 执行时边读输入边写输出,
// 两者重叠会导致结果错误;
// - 绑定方式:make_ref<BlobObj>(runtime, (char *)base + offset)。
std::map<Tensor, size_t> refCount; // 每个 Tensor 的消费者总数
std::map<Tensor, size_t> offsetMap; // 每个 Tensor 的偏移
std::unordered_set<Tensor> allocated; // 已分配的 Tensor

for(auto &t: tensors) {
refCount[t] = t->getTargets().size();
}
// 阶段一:模拟分配,规划每个 tensor 的偏移量
for(auto& op : ops) {
// 为算子的输入分配内存(每个 tensor 只规划一次)
for(auto& t : op->getInputs()) {
if(allocated.find(t) == allocated.end()) {
offsetMap[t] = allocator.alloc(t->getBytes());
allocated.insert(t);
}
}

// 为算子的输出分配内存(先于输入回收,避免与输入重叠)
for(auto& t : op->getOutputs()) {
if(allocated.find(t) == allocated.end()) {
offsetMap[t] = allocator.alloc(t->getBytes());
allocated.insert(t);
}
}
// 本算子的输入在本算子执行完后不再被它使用:
// 引用计数减一,归零的输入(最后一个消费者已完成)立即回收,
// 供后面的算子复用,从而降低峰值内存。
for(auto& t : op->getInputs()) {
--refCount[t];
if(refCount[t] == 0) {
allocator.free(offsetMap[t], t->getBytes());
}
}
}
// 阶段二:真正分配内存,并按偏移量给每个 tensor 绑定 Blob
auto base = allocator.getPtr();
for(auto &[t, offset] : offsetMap) {
t->setDataBlob(make_ref<BlobObj>(runtime, (char*)base + offset));
}

allocator.info();
}

Expand Down
20 changes: 20 additions & 0 deletions src/operators/concat.cc
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
#include "operators/concat.h"
#include "utils/operator_utils.h"
#include <cstddef>

namespace infini {
ConcatObj::ConcatObj(GraphObj *graph, TensorVec inputs, Tensor output, int _dim)
Expand All @@ -17,6 +18,25 @@ optional<vector<Shape>> ConcatObj::inferShape(const TensorVec &inputs) {
// TODO:修改 dims,返回正确的 concat 后的 shape
// REF: https://onnx.ai/onnx/operators/onnx__Concat.html#concat-13
// =================================== 作业 ===================================
// 除 dim 维外其它维度保持不变,dim 维大小为所有输入在该维大小之和;
// 构造函数已把 dim 归一化到 [0, rank),实现里再用 get_real_axis(dim, rank)
// 防御一次负轴更稳妥;
// 注意先 dims[dim] = 0 再累加,且所有输入的 rank 必须一致。
// 踩坑:除 dim 维外,其它维度也必须逐一断言相等(ONNX 要求所有输入除
// concat 维外形状相同),只检查 rank 是不够的。
// 注意:上面 auto rank 推导出来的类型是 size_t 即unsigned long
size_t concatDim = get_real_axis(dim, rank);
dims[dim] = 0;
for(auto& input : inputs) {
IT_ASSERT(input->getRank() == rank);
for (size_t i = 0; i < rank; ++i) {
if (i != concatDim) { // 除拼接维外,其余维度必须相同
IT_ASSERT(input->getDims()[i] == dims[i],
"Concat: mismatch on dim " + std::to_string(i));
}
}
dims[concatDim] += (input->getDims())[concatDim];
}

return {{dims}};
}
Expand Down
Loading