CHAPTER 43 / Tenstorrent · 显式数据流

Reader、compute、writer:缓冲区里的生产与消费

一块tile何时能读,何时才能复用?

阅读与推演约 55 分钟练习时间另计

这一章要弄清楚

  • 区分reserve、publish、wait与consume
  • 解释NoC搬运完成和buffer可读的关系
  • 用有限容量分析阻塞与死锁

先备知识:条件变量、有界队列与关闭协议 / 原子操作与 happens-before / Tensix 与 tiles:先安排数据,再安排计算

C++20 本机逻辑模型;不是设备仿真或性能测量。厂商语法片段未在设备或 SDK 执行,实际 API 以匹配版本的公开文档为准。

三段工作为什么分开

考虑逐块执行加一。Reader把输入从较远的存储位置搬到本地,compute读取本地输入并算出输出,writer把结果搬到目的地。三段分开后,有机会在计算当前块时准备下一块。真正难点是各段速度不同:计算还没读完,reader不能覆盖;writer还没写完,compute不能随意复用输出空间。

Circular buffer可以先理解为容量有限的先进先出区域。环形只说明物理位置会回绕,不说明读写一定安全。协议必须区分空闲空间、已预留但未完成的数据、已发布数据以及已消费空间。例一容量为二,连续放入10、20后已满;取走10才有位置放30,剩余消费顺序仍是20、30。

预留与发布是两次不同动作

Metalium的公开CB接口使用reserve/push和wait/pop等操作表达这些关系。预留空间使生产者获得写入位置;填入数据并满足搬运完成条件之后,才能发布可消费项。消费者先等待足够已发布数据,使用结束后再释放相应位置。不能把拿到地址理解为数据已经完成。具体到cb_reserve_back,它阻塞等待足够空位,本身不前移任何CB指针;实际发布与写入侧推进由cb_push_back完成。这里的“预留”也不是一个可让任意多个生产者并发抢占空间的通用锁。

// CB生产侧顺序示意,未在SDK/设备执行。
// in_cb已配置;reserve/push数量符合对应版本的容量约束。
cb_reserve_back(in_cb, 1);
const auto destination = get_write_ptr(in_cb);
// 在此提交写入destination的NoC读取,参数取决于地址生成器。
noc_async_read_barrier();
cb_push_back(in_cb, 1);

片段故意没有编造适用于所有版本的NoC地址构造调用。完整程序必须先正确发起搬运;barrier等待已提交操作,不能凭空生成数据。消费者侧相应地等待已发布tile,完成读取后pop。实际kernel还需要配置一致的CB编号、格式、页面大小与处理数量。

NoC是通信路径,不是全局共享变量

NoC把数据在芯片节点间移动。一次异步搬运提交后,生产者需要知道什么时候目标本地数据可用,才可以通知compute。通知过早会让计算读到旧数据;过晚可能降低重叠。例二用reserved、transferred、published三个布尔状态展示这种先后关系,不模拟网络延迟、带宽或硬件流控。

NoC请求中的地址、长度和目标buffer容量也都是合同。元素数量和字节数量混淆,常导致只复制部分tile;配置两页但按三页发布,可能导致指针与可用量错位。调试时从一块tile开始,核对生产数、消费数及每次状态变化,比一开始运行整个矩阵更容易定位。

有限容量会改变程序能否前进

假设输入和输出buffer都只有一页,compute只有在输出有空位时才能完成工作。如果writer等待一个永远不会到来的额外tile,输出空间一直不释放,compute停住,reader也可能最终停住。这不是“设备慢”,而是等待图中出现了无法满足的条件。

分析死锁时逐段列出正在等待什么、由谁产生、总共应该生产多少。把所有buffer变大也许掩盖问题,却不修复数量或依赖错误。正确协议应在规定容量和边界规模下仍能结束;随后才用设备trace与profiling评估吞吐。

当前能验收什么

本章CPU模型检查FIFO顺序与发布条件,未执行线程、NoC或Metalium。它提供一种读代码的方法:每次找到reserve就追问谁填数据,找到push就追问数据是否完成,找到pop就追问最后一次使用是否结束。能逐项回答后,再去阅读对应版本的完整reader/compute/writer示例。

观察 · 推演

单tile的所有权转移

reserved1transferred0published001 / 03 · TIMELINEreserved1transferred0published001 / 03 · TIMELINE
预留

消费者不可读。

1 / 3
阅读完整推演文字
  1. 预留

    reserved:1;transferred:0;published:0

    消费者不可读。

  2. 搬运完成

    payload:7;transferred:1;published:0

    payload=7,但尚未发布。

  3. 发布

    published:1;consumer:7

    消费者此时可读7。

跟着例子,走完一遍

例题 01C++20 · 本机可运行

容量二的FIFO回绕

放10、20,取10,再放30。

  1. 填满两个槽
  2. 消费第一项释放槽
  3. 新值复用槽,顺序保持
39-a.cpp
下载
// Original CPU teaching model; not a device simulator or benchmark.
#include <iostream>
#include <array>
#include <cstddef>
#include <stdexcept>

void require(bool ok) { if (!ok) throw std::runtime_error("model check failed"); }

int main() {
 std::array<int,2> slots{}; std::size_t head=0,tail=0,count=0;
 auto push=[&](int v){require(count<2);slots[tail]=v;tail=(tail+1)%2;++count;};
 auto pop=[&](){require(count>0);int v=slots[head];head=(head+1)%2;--count;return v;};
 push(10);push(20);const int a=pop();push(30);const int b=pop(),c=pop();
 require(a==10 && b==20 && c==30 && count==0);
 std::cout<<"order="<<a<<' '<<b<<' '<<c<<'\n';
}

如何编译和运行下载的 .cpp 文件 →

结果与解释

输出顺序10 20 30。

回绕改变物理位置,不改变FIFO顺序。

在本机运行这个例子

下载后,在文件所在目录执行。需要支持 C++20 的编译器;POSIX 示例还需要章节说明中的系统条件。

clang++ -std=c++20 -Wall -Wextra -Wpedantic -Werror -pthread 39-a.cpp -o example && ./example

预期标准输出:

order=10 20 30
例题 02C++20 · 本机可运行

预留不代表可读

模拟一块数据从预留到发布,检查早发布被拒绝。

  1. reserved=true
  2. 搬运未完成不能publish
  3. 写入7后才允许发布
39-b.cpp
下载
// Original CPU teaching model; not a device simulator or benchmark.
#include <iostream>
#include <stdexcept>

void require(bool ok) { if (!ok) throw std::runtime_error("model check failed"); }

int main() {
 bool reserved=true,transferred=false,published=false;
 const bool early=reserved && transferred;
 require(!early);
 int payload=7; transferred=true;
 published=reserved && transferred;
 require(published && payload==7);
 std::cout<<"early_publish=blocked consumer="<<payload<<'\n';
}
结果与解释

early_publish=blocked,consumer=7。

检查协议先后,不模拟NoC或并发内存序。

在本机运行这个例子

下载后,在文件所在目录执行。需要支持 C++20 的编译器;POSIX 示例还需要章节说明中的系统条件。

clang++ -std=c++20 -Wall -Wextra -Wpedantic -Werror -pthread 39-b.cpp -o example && ./example

预期标准输出:

early_publish=blocked consumer=7
常见错误

把预留当发布

刚获得写指针就允许compute读取。

修正思路:填入数据并等待必要搬运完成,再发布;消费完才释放。

轮到你动手

先写预测或代码,再按需打开提示。完整答案用于对照自己的推理。

练习 1

reader生产5块,compute等待6块后才处理,会发生什么?

给我一点提示
  1. 是否还有第6块的生产者
  2. 区分慢和不可能
查看答案与推理

如果没有其他生产者,等待条件永远不满足;必须统一总块数,或设计明确尾批协议。增加等待时间和buffer容量都不能生成第6块。

练习 2

给一块tile画四个时点并说明谁可访问。

给我一点提示
  1. reserve后还不能消费
  2. pop后不能继续读
查看答案与推理

空闲→reserve后仅生产者写→搬运完成并push后消费者可读→消费者结束并pop后空间可复用。真实接口可能把阶段分得更细,但每次所有权转移都必须有完成条件。

把理解说出来

先用中文讲清因果,再用英文回答。问题依据技能主题编写,并非公司内部题库。

解释

reserve和push有什么区别?

参考回答 / English answer

cb_reserve_back等待足够空位,本身不前移CB指针;写入并满足完成条件后,cb_push_back发布数据并推进写入侧状态。它不是支持任意多生产者并发抢占的通用预留操作。

cb_reserve_back waits for free space without advancing a CB pointer. After data is ready, cb_push_back publishes it and advances the producer-side state.
调试

异步NoC读取后立即push,可能有什么错误?

参考回答 / English answer

compute可能读到尚未完成搬运的数据;需遵守完成与发布协议。

The compute kernel may read data before transfer completion. Publication must follow the required completion condition.
追问

pop为何要晚于最后一次读取?

参考回答 / English answer

pop允许生产者复用空间,提前释放会导致旧消费者读到新内容。

Popping permits the producer to reuse storage. It must follow the consumer's last use.
预测

容量二:放1、2,取1,放3,接着取什么?

参考回答 / English answer

先2后3,物理槽回绕不改变FIFO。

The next values are two and then three. Physical wraparound does not change FIFO order.
调试

程序停住时只增加CB容量合理吗?

参考回答 / English answer

先查生产消费数量及等待图;容量增大可能掩盖永远不满足的条件。

I inspect item counts and the wait graph first. Larger buffers can hide a protocol error without fixing it.
解释

本机FIFO通过是否证明Metalium kernel正确?

参考回答 / English answer

不证明。真实配置、NoC、同步、数据格式和设备执行仍需独立验证。

The CPU model verifies a limited protocol idea. Device configuration, transfers, and kernel synchronization remain untested.

继续查证

公开资料用于查证;本章图解和例题是独立教学内容。CPU 逻辑模型不能证明设备性能。