2 · Tensor、设备与内存:值是如何被搬运的
从 Python EagerTensor 到 C++ TensorBuffer、allocator 和 device placement
Tensor 不是一块裸内存
C++ tensorflow::Tensor 至少把四件事绑在一起:DataType、TensorShape、TensorBuffer 和分配器/所有权语义。Tensor::dtype() 与 shape() 是元数据,tensor<T>() / flat<T>() 才是类型化数据访问;复制 Tensor 通常复制的是 buffer 引用或触发写时分离策略,而不是每次都深拷贝全部元素。
Tensor
├─ TensorShape rank + dims + element count
├─ DataType float32 / int64 / resource / variant ...
├─ TensorBuffer data pointer + ref count + allocation metadata
└─ Device placement 该 buffer 当前属于哪个 device / memory spacetensorflow/core/framework/tensor.h 中的 TensorBuffer 使用引用计数,并提供 root_buffer()、内存大小和 allocation description 等接口。这样 runtime 可以在不暴露具体 allocator 的情况下做内存统计、别名判断、临时 buffer 和跨设备复制。
Python 包装的两种身份
tensorflow/python/framework/ops.py 同时处理:
SymbolicTensor:指向 Graph 中某个Operation的输出,拥有op、graph、value_index,不能在 Python 中直接.numpy();- Eager Tensor:由 C API handle 支撑,可在执行完成后
.numpy(),但仍携带 dtype、shape、device 和生命周期信息。
这两个对象共享很多 Python 运算符入口,却不能混用。tf.function tracing 的核心工作之一,就是把 eager 输入替换成 placeholder-like 的符号输入,使同一份 Python 代码可以生成 Graph。
设备放置的两步
- Python 侧的
tf.device、Context策略和输入 Tensor 提供偏好; - C++ runtime 基于 op 的 kernel 注册、设备可用性、colocation、软放置和复制策略决定实际执行位置。
“请求 GPU”不等于“最终一定在 GPU”:如果某个 dtype/op 没有 GPU kernel,soft placement 可能把它放到 CPU;如果输入来自不同设备,还会插入隐式 copy 或把 op 放到能满足约束的共同设备。
内存生命周期的真正边界
一个 Tensor 可能经历:Python handle → C API tensor handle → runtime Tensor → device buffer → output handle。异步 executor 下,Python 调用返回时 device kernel 可能还没完成;.numpy()、显式同步或依赖后续操作会形成等待点。EagerExecutor 用节点队列和状态记录 pending、finished、error,避免把每次 Python 调用都强制变成全局同步。
源码锚点
tensorflow/core/framework/tensor.h:Tensor、TensorBuffer、shape/dtype/data API。tensorflow/core/framework/tensor_shape.h:维度、未知维度和 element count。tensorflow/python/framework/ops.py:SymbolicTensor、eager tensor 的 Python 侧协议。tensorflow/python/eager/context.py:设备列表、线程本地 device stack、placement policy。tensorflow/core/common_runtime/eager/eager_executor.h:异步节点、等待和错误传播。tensorflow/core/common_runtime/device_set.h:设备集合与选择相关抽象。
观察实验
tf.debugging.set_log_device_placement(True)
x = tf.ones([1024, 1024])
y = tf.ones([1024, 1024])
z = tf.matmul(x, y)
_ = z.numpy() # 把异步执行结果拉回 Python观察日志时,把“op 的 requested device”“实际 kernel device”和“数据搬运”分开看;三者不一定相同。