CHAPTER 43 / Tenstorrent · 显式数据流
Reader、compute、writer:缓冲区里的生产与消费
一块tile何时能读,何时才能复用?
这一章要弄清楚
- 区分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的所有权转移
消费者不可读。
payload=7,但尚未发布。
消费者此时可读7。
阅读完整推演文字
- 预留
reserved:1;transferred:0;published:0
消费者不可读。
- 搬运完成
payload:7;transferred:1;published:0
payload=7,但尚未发布。
- 发布
published:1;consumer:7
消费者此时可读7。
跟着例子,走完一遍
容量二的FIFO回绕
放10、20,取10,再放30。
- 填满两个槽
- 消费第一项释放槽
- 新值复用槽,顺序保持
// 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';
}
输出顺序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
预留不代表可读
模拟一块数据从预留到发布,检查早发布被拒绝。
- reserved=true
- 搬运未完成不能publish
- 写入7后才允许发布
// 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块后才处理,会发生什么?
给我一点提示
- 是否还有第6块的生产者
- 区分慢和不可能
查看答案与推理
如果没有其他生产者,等待条件永远不满足;必须统一总块数,或设计明确尾批协议。增加等待时间和buffer容量都不能生成第6块。
练习 2
给一块tile画四个时点并说明谁可访问。
给我一点提示
- reserve后还不能消费
- 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.继续查证
- CB reserve API ↗
Capacity and reservation contract
- Metalium kernel API index ↗
Circular buffer and NoC APIs
- CB push API ↗
Publication, pointer advancement and wrap constraints
公开资料用于查证;本章图解和例题是独立教学内容。CPU 逻辑模型不能证明设备性能。