A DEVICE link, and why NOT_HOST is permanent (L0/L1 substrate)¶
A segment carries the address space of the backend that made it. HOST bytes are
CPU-addressable; DEVICE bytes — GPU or accelerator memory
(ADR-0024)
— are not, and the codec must never dereference them. A rope may therefore be
heterogeneous: a host header link chained to a device payload link. all_host() is the one
question a host-side operation asks before it touches a byte.
What to notice¶
NOT_HOSTis a property of the rope, not of the moment. No retry ever fixes it; the payload has to leave via its device path. That is a different verdict fromNO_MEMORY(the pool page), which is transient — and keeping them apart is exactly what #917 bought.The refusal is about the link, not the rope type. The example takes the host sub-range of the same rope and it is host, and flattenable. Nothing about a heterogeneous rope is poisoned wholesale.
borrow_devicetags ordinary host memoryDEVICE. A vendor-free stand-in: it registers no byte-mover, somem::transferdeclines it — and it declines it for the space tag, which is the same refusal a real device link gets. Every verdict on this page is deterministic in a stock build with no accelerator present.The real device backend is a
backends/-tier module. It registers its own transfer hook throughregister_device_backend; core assigns the space and nothing else. That tiering is why this example needs no CUDA and takes no skip.Nothing here is conditional — the target builds and runs under every CI leg.
Source¶
1/*
2 * SPDX-License-Identifier: Apache-2.0
3 * SPDX-FileCopyrightText: Copyright 2026 avatarsd LLC
4 */
5
6/**
7 * @file
8 * @brief ONE CONCEPT — a DEVICE link, and why refusing to flatten it is PERMANENT.
9 *
10 * A segment carries the address space of the backend that made it. `HOST` bytes are
11 * CPU-addressable; `DEVICE` bytes (GPU/accelerator memory, ADR-0024) are not, and the codec
12 * must never dereference them. A rope may be HETEROGENEOUS — a host header link chained to a
13 * device payload link — and `all_host()` is the one question a host-side operation asks
14 * before touching bytes.
15 *
16 * The pay-off is in the error channel. `flatten_err_t::NOT_HOST` is a property of the rope
17 * itself: no retry ever fixes it, and the payload must go out via its device path. That is a
18 * different verdict from `NO_MEMORY`, which is transient backpressure (see
19 * `view_pool_backend`) — collapsing the two is exactly what let a local OOM be reported to a
20 * peer as a malformed frame (#917).
21 *
22 * `borrow_device` tags ordinary host memory `DEVICE` and registers no byte-mover, so
23 * `mem::transfer` refuses it — a real device backend lives in the `backends/` tier and
24 * registers its own. Nothing here is conditional: every verdict below is deterministic in a
25 * stock build with no accelerator present.
26 *
27 * Runs under ctest as `example_view_device_rope`; returns non-zero on any failed check.
28 */
29
30#include <array>
31#include <cstddef>
32#include <cstdio>
33#include <optional>
34#include <span>
35
36#include "libtracer/tracer.hpp"
37
38namespace {
39
40/** @brief Report expectation @p what and record a failure on @p ok. */
41void check(bool& ok, bool cond, const char* what) {
42 std::printf(" [%s] %s\n", cond ? "ok" : "FAIL", what);
43 ok = ok && cond;
44}
45
46} // namespace
47
48int main() {
49 bool ok = true;
50 std::array<std::byte, 8> header_bytes{};
51 std::array<std::byte, 8> payload_bytes{};
52
53 const tr::view::view_t host = tr::view::view_t::over(tr::view::borrow(header_bytes));
54 tr::view::segment_ptr_t dev_seg = tr::view::borrow_device(payload_bytes);
55 const tr::view::view_t device = tr::view::view_t::over(dev_seg);
56 check(ok, host.is_host() && !host.is_device(), "a borrowed host link is CPU-addressable");
57 check(ok, device.is_device(), "a borrow_device link reports DEVICE space");
58
59 tr::view::rope_t frame;
60 frame.append(host);
61 frame.append(device);
62 std::printf("heterogeneous rope: %zu links, %zu bytes, all_host=%d\n", frame.link_count(),
63 frame.total_length(), static_cast<int>(frame.all_host()));
64 check(ok, !frame.all_host(), "one DEVICE link makes the whole rope non-host");
65
66 const auto refused = frame.try_flatten();
67 check(ok, !refused && refused.error() == tr::view::flatten_err_t::NOT_HOST,
68 "try_flatten refuses it as NOT_HOST — permanent, and never a retry");
69
70 // The out-of-core arm: no backend registered a byte-mover for this segment, so the
71 // transfer is declined by value rather than guessed at with a memcpy.
72 std::array<std::byte, 8> staging{};
73 check(ok, !tr::mem::transfer(dev_seg.get(), staging, tr::mem::io_dir_t::DEVICE_TO_CPU),
74 "and mem::transfer declines a DEVICE segment no backends/ module claims");
75
76 // The host half of the same rope still flattens — the refusal is about the link, not the
77 // rope type.
78 check(ok, frame.subrope(0, 8).all_host(), "the host sub-range is host, and stays flattenable");
79 return ok ? 0 : 1;
80}
See also: backends · views module · memory substrate reference.