diff --git a/.agents/skills/tenferro-compute/SKILL.md b/.agents/skills/tenferro-compute/SKILL.md index 0e9d45841..92ba0c8b7 100644 --- a/.agents/skills/tenferro-compute/SKILL.md +++ b/.agents/skills/tenferro-compute/SKILL.md @@ -28,9 +28,19 @@ not when changing tenferro itself. ## Non-negotiable defaults -- Dense flat buffers are column-major: the leftmost dimension varies fastest. -- Bind one backend/runtime/compiler and reuse it across related work. -- Compile traced programs once outside repeated execution loops. +- **Column-major storage.** Dense buffers are column-major: the leftmost + dimension varies fastest. Row-major data passed to `from_vec_col_major` is + silently reinterpreted as column-major — permuted/wrong values, never + rejected. +- **No facade crate.** `cargo add tenferro` fails by design; depend on the + crates you need (`tenferro-runtime`, `tenferro-cpu`, and operation crates). +- **Explicit backend.** Direct operations take an explicit backend argument; + construct the backend/runtime once and reuse it — per-call construction + discards the buffer pool. +- **Einsum dialect.** Equations need the explicit arrow (`"ij,jk->ik"`); `...` + ellipsis is unsupported. +- **Result-returning operators.** Traced operators return `Result`; propagate + with `?`. - CPU/GPU transfers are explicit; unsupported GPU operations do not silently fall back to CPU. - Traced standard extensions need an explicitly installed extension module and diff --git a/.agents/skills/tenferro-compute/references/api-cheatsheet.md b/.agents/skills/tenferro-compute/references/api-cheatsheet.md index 8d72d392f..d69d61d4d 100644 --- a/.agents/skills/tenferro-compute/references/api-cheatsheet.md +++ b/.agents/skills/tenferro-compute/references/api-cheatsheet.md @@ -113,3 +113,47 @@ Import recipes: If a method is present in the guide but Rust reports E0599, check the owning crate's root re-exports and bring its `*Ext` trait into the local module. + +## Borrowing external memory + +Wrap an existing column-major buffer (a `faer::Mat`, an ndarray view, or any +slice plus strides) zero-copy with `TypedTensorView::from_slice`; use +`TypedTensorViewMut::from_slice` to write through the borrowed buffer. This is +how a tenferro kernel consumes memory owned by another library without a +migration: no tensor ownership is transferred. + + +```rust +use tenferro_runtime::TypedTensorView; +use tenferro_tensor::TypedTensorViewMut; + +// Wrap a column-major faer::Mat without copying. faer pads columns to +// alignment, so the borrowed slice spans `col_stride * ncols` elements and the +// column stride is passed explicitly; the padding is never read logically. +let mat = faer::Mat::from_fn(2, 3, |r, c| (r * 3 + c) as f64); +let data = unsafe { std::slice::from_raw_parts(mat.as_ref().as_ptr(), (mat.col_stride() as usize) * mat.ncols()) }; +let view = TypedTensorView::from_slice(vec![2, 3], vec![1_isize, mat.col_stride()], 0, data)?; +assert_eq!(view.get(&[1, 2]), Some(&5.0)); + +// Wrap an ndarray row-major view the same way. Strides are arbitrary, so a +// row-major buffer is not transposed; it is only wrapped. +let arr = ndarray::Array2::from_shape_vec((2, 3), (0..6).map(|i| i as f64).collect())?; +let nview = TypedTensorView::from_slice(arr.shape(), arr.strides(), 0, arr.as_slice().expect("row-major array is contiguous"))?; +assert_eq!(nview.get(&[0, 2]), Some(&2.0)); + +// The mutable variant writes through the borrowed buffer. +let mut buffer = [0.0_f64, 0.0, 0.0, 0.0]; +let mut mview = TypedTensorViewMut::from_slice(vec![2, 2], vec![1, 2], 0, &mut buffer)?; +*mview.get_mut(&[0, 0]).expect("in-bounds index") = 7.0; +assert_eq!(buffer[0], 7.0); +``` + + +Because strides are arbitrary, row-major data wraps without transposition — +but kernels are tuned for column-major contiguity. Materialize a copy when the +wrapped buffer is row-major or its lifetime ends before the operation; +`TypedTensorView::duplicate()` requires a column-major-contiguous view, so for +a row-major wrap copy into a fresh `TypedTensor` (or an owned `Vec` in +column-major order) instead. If you search for the retired +`TypedStridedTensorView` (tenferro-rs#886), stop: `TypedTensorView::from_slice` +is its successor. diff --git a/.agents/skills/tenferro-compute/references/pitfalls.md b/.agents/skills/tenferro-compute/references/pitfalls.md index 29340efda..2482476cd 100644 --- a/.agents/skills/tenferro-compute/references/pitfalls.md +++ b/.agents/skills/tenferro-compute/references/pitfalls.md @@ -38,3 +38,48 @@ is in the [traced API example](api-cheatsheet.md#traced-tensors-and-extensions). `blas-accelerate`) when using `cpu-blas`. - CUDA is explicit: enable `tenferro-gpu`'s `cuda` feature and upload CPU tensors before CUDA operations. There is no implicit transfer. + +## Per-origin traps + +Same traps, keyed by the priors you arrived with. + +### If you come from ndarray / NumPy + +- Your buffers are row-major; passing them to `from_vec_col_major` silently + reinterprets them as column-major (no error, permuted/wrong values). Reorder + explicitly or wrap with + `TypedTensorView::from_slice` (see the [API cheatsheet](api-cheatsheet.md#borrowing-external-memory)). +- `cargo add tenferro` fails: there is no facade crate. Add + `tenferro-runtime` + `tenferro-cpu` (plus operation crates). +- `Array2::dot` is a free-standing habit; tenferro direct ops take an explicit + `&mut CpuBackend` argument: `a.matmul(&b, &mut backend)`. +- Your priors do not include a dtype-erased tensor; `Tensor` (runtime dtype) + has no ndarray counterpart — use `TypedTensor`. + +### If you come from nalgebra + +- nalgebra is column-major like tenferro, so `.data` / `as_slice()` buffers map + directly to `from_vec_col_major`. +- `Matrix::dot` / `gemm` are methods without an execution context; tenferro + direct ops need the explicit backend argument. Eager and traced tiers drop + it: `EagerTensor` methods run through the `EagerRuntime`, traced methods + build a graph. +- A single `DMatrix` maps to `TypedTensor`; there is no separate runtime + dtype type unless you actually need runtime dtype selection. + +### If you come from PyTorch / JAX + +- Trace one level of `?`: traced operators return `Result`; `a.matmul(&b)` is + a `Result`, unlike `torch.matmul` / `jnp.matmul`. +- Einsum needs the explicit arrow and rejects `...` (see + [Einsum syntax](#einsum-syntax)). +- The backend is not ambient: eager code must own an `EagerRuntime`, traced + code must register an engine plus extension modules (see + [Extension registration](#extension-registration)). + +## Retired names + +Searching for the retired `TypedStridedTensorView` (tenferro-rs#886) leads +nowhere: the name survives only in the obsolete-names vocabulary test. The +zero-copy view API is `TypedTensorView::from_slice` / `TypedTensorViewMut::from_slice` +(see the [API cheatsheet](api-cheatsheet.md#borrowing-external-memory)). diff --git a/.claude/skills/tenferro-compute/SKILL.md b/.claude/skills/tenferro-compute/SKILL.md index 0e9d45841..92ba0c8b7 100644 --- a/.claude/skills/tenferro-compute/SKILL.md +++ b/.claude/skills/tenferro-compute/SKILL.md @@ -28,9 +28,19 @@ not when changing tenferro itself. ## Non-negotiable defaults -- Dense flat buffers are column-major: the leftmost dimension varies fastest. -- Bind one backend/runtime/compiler and reuse it across related work. -- Compile traced programs once outside repeated execution loops. +- **Column-major storage.** Dense buffers are column-major: the leftmost + dimension varies fastest. Row-major data passed to `from_vec_col_major` is + silently reinterpreted as column-major — permuted/wrong values, never + rejected. +- **No facade crate.** `cargo add tenferro` fails by design; depend on the + crates you need (`tenferro-runtime`, `tenferro-cpu`, and operation crates). +- **Explicit backend.** Direct operations take an explicit backend argument; + construct the backend/runtime once and reuse it — per-call construction + discards the buffer pool. +- **Einsum dialect.** Equations need the explicit arrow (`"ij,jk->ik"`); `...` + ellipsis is unsupported. +- **Result-returning operators.** Traced operators return `Result`; propagate + with `?`. - CPU/GPU transfers are explicit; unsupported GPU operations do not silently fall back to CPU. - Traced standard extensions need an explicitly installed extension module and diff --git a/.claude/skills/tenferro-compute/references/api-cheatsheet.md b/.claude/skills/tenferro-compute/references/api-cheatsheet.md index 8d72d392f..d69d61d4d 100644 --- a/.claude/skills/tenferro-compute/references/api-cheatsheet.md +++ b/.claude/skills/tenferro-compute/references/api-cheatsheet.md @@ -113,3 +113,47 @@ Import recipes: If a method is present in the guide but Rust reports E0599, check the owning crate's root re-exports and bring its `*Ext` trait into the local module. + +## Borrowing external memory + +Wrap an existing column-major buffer (a `faer::Mat`, an ndarray view, or any +slice plus strides) zero-copy with `TypedTensorView::from_slice`; use +`TypedTensorViewMut::from_slice` to write through the borrowed buffer. This is +how a tenferro kernel consumes memory owned by another library without a +migration: no tensor ownership is transferred. + + +```rust +use tenferro_runtime::TypedTensorView; +use tenferro_tensor::TypedTensorViewMut; + +// Wrap a column-major faer::Mat without copying. faer pads columns to +// alignment, so the borrowed slice spans `col_stride * ncols` elements and the +// column stride is passed explicitly; the padding is never read logically. +let mat = faer::Mat::from_fn(2, 3, |r, c| (r * 3 + c) as f64); +let data = unsafe { std::slice::from_raw_parts(mat.as_ref().as_ptr(), (mat.col_stride() as usize) * mat.ncols()) }; +let view = TypedTensorView::from_slice(vec![2, 3], vec![1_isize, mat.col_stride()], 0, data)?; +assert_eq!(view.get(&[1, 2]), Some(&5.0)); + +// Wrap an ndarray row-major view the same way. Strides are arbitrary, so a +// row-major buffer is not transposed; it is only wrapped. +let arr = ndarray::Array2::from_shape_vec((2, 3), (0..6).map(|i| i as f64).collect())?; +let nview = TypedTensorView::from_slice(arr.shape(), arr.strides(), 0, arr.as_slice().expect("row-major array is contiguous"))?; +assert_eq!(nview.get(&[0, 2]), Some(&2.0)); + +// The mutable variant writes through the borrowed buffer. +let mut buffer = [0.0_f64, 0.0, 0.0, 0.0]; +let mut mview = TypedTensorViewMut::from_slice(vec![2, 2], vec![1, 2], 0, &mut buffer)?; +*mview.get_mut(&[0, 0]).expect("in-bounds index") = 7.0; +assert_eq!(buffer[0], 7.0); +``` + + +Because strides are arbitrary, row-major data wraps without transposition — +but kernels are tuned for column-major contiguity. Materialize a copy when the +wrapped buffer is row-major or its lifetime ends before the operation; +`TypedTensorView::duplicate()` requires a column-major-contiguous view, so for +a row-major wrap copy into a fresh `TypedTensor` (or an owned `Vec` in +column-major order) instead. If you search for the retired +`TypedStridedTensorView` (tenferro-rs#886), stop: `TypedTensorView::from_slice` +is its successor. diff --git a/.claude/skills/tenferro-compute/references/pitfalls.md b/.claude/skills/tenferro-compute/references/pitfalls.md index 29340efda..2482476cd 100644 --- a/.claude/skills/tenferro-compute/references/pitfalls.md +++ b/.claude/skills/tenferro-compute/references/pitfalls.md @@ -38,3 +38,48 @@ is in the [traced API example](api-cheatsheet.md#traced-tensors-and-extensions). `blas-accelerate`) when using `cpu-blas`. - CUDA is explicit: enable `tenferro-gpu`'s `cuda` feature and upload CPU tensors before CUDA operations. There is no implicit transfer. + +## Per-origin traps + +Same traps, keyed by the priors you arrived with. + +### If you come from ndarray / NumPy + +- Your buffers are row-major; passing them to `from_vec_col_major` silently + reinterprets them as column-major (no error, permuted/wrong values). Reorder + explicitly or wrap with + `TypedTensorView::from_slice` (see the [API cheatsheet](api-cheatsheet.md#borrowing-external-memory)). +- `cargo add tenferro` fails: there is no facade crate. Add + `tenferro-runtime` + `tenferro-cpu` (plus operation crates). +- `Array2::dot` is a free-standing habit; tenferro direct ops take an explicit + `&mut CpuBackend` argument: `a.matmul(&b, &mut backend)`. +- Your priors do not include a dtype-erased tensor; `Tensor` (runtime dtype) + has no ndarray counterpart — use `TypedTensor`. + +### If you come from nalgebra + +- nalgebra is column-major like tenferro, so `.data` / `as_slice()` buffers map + directly to `from_vec_col_major`. +- `Matrix::dot` / `gemm` are methods without an execution context; tenferro + direct ops need the explicit backend argument. Eager and traced tiers drop + it: `EagerTensor` methods run through the `EagerRuntime`, traced methods + build a graph. +- A single `DMatrix` maps to `TypedTensor`; there is no separate runtime + dtype type unless you actually need runtime dtype selection. + +### If you come from PyTorch / JAX + +- Trace one level of `?`: traced operators return `Result`; `a.matmul(&b)` is + a `Result`, unlike `torch.matmul` / `jnp.matmul`. +- Einsum needs the explicit arrow and rejects `...` (see + [Einsum syntax](#einsum-syntax)). +- The backend is not ambient: eager code must own an `EagerRuntime`, traced + code must register an engine plus extension modules (see + [Extension registration](#extension-registration)). + +## Retired names + +Searching for the retired `TypedStridedTensorView` (tenferro-rs#886) leads +nowhere: the name survives only in the obsolete-names vocabulary test. The +zero-copy view API is `TypedTensorView::from_slice` / `TypedTensorViewMut::from_slice` +(see the [API cheatsheet](api-cheatsheet.md#borrowing-external-memory)). diff --git a/.kimi/skills/tenferro-compute/SKILL.md b/.kimi/skills/tenferro-compute/SKILL.md index 0e9d45841..92ba0c8b7 100644 --- a/.kimi/skills/tenferro-compute/SKILL.md +++ b/.kimi/skills/tenferro-compute/SKILL.md @@ -28,9 +28,19 @@ not when changing tenferro itself. ## Non-negotiable defaults -- Dense flat buffers are column-major: the leftmost dimension varies fastest. -- Bind one backend/runtime/compiler and reuse it across related work. -- Compile traced programs once outside repeated execution loops. +- **Column-major storage.** Dense buffers are column-major: the leftmost + dimension varies fastest. Row-major data passed to `from_vec_col_major` is + silently reinterpreted as column-major — permuted/wrong values, never + rejected. +- **No facade crate.** `cargo add tenferro` fails by design; depend on the + crates you need (`tenferro-runtime`, `tenferro-cpu`, and operation crates). +- **Explicit backend.** Direct operations take an explicit backend argument; + construct the backend/runtime once and reuse it — per-call construction + discards the buffer pool. +- **Einsum dialect.** Equations need the explicit arrow (`"ij,jk->ik"`); `...` + ellipsis is unsupported. +- **Result-returning operators.** Traced operators return `Result`; propagate + with `?`. - CPU/GPU transfers are explicit; unsupported GPU operations do not silently fall back to CPU. - Traced standard extensions need an explicitly installed extension module and diff --git a/.kimi/skills/tenferro-compute/references/api-cheatsheet.md b/.kimi/skills/tenferro-compute/references/api-cheatsheet.md index 8d72d392f..d69d61d4d 100644 --- a/.kimi/skills/tenferro-compute/references/api-cheatsheet.md +++ b/.kimi/skills/tenferro-compute/references/api-cheatsheet.md @@ -113,3 +113,47 @@ Import recipes: If a method is present in the guide but Rust reports E0599, check the owning crate's root re-exports and bring its `*Ext` trait into the local module. + +## Borrowing external memory + +Wrap an existing column-major buffer (a `faer::Mat`, an ndarray view, or any +slice plus strides) zero-copy with `TypedTensorView::from_slice`; use +`TypedTensorViewMut::from_slice` to write through the borrowed buffer. This is +how a tenferro kernel consumes memory owned by another library without a +migration: no tensor ownership is transferred. + + +```rust +use tenferro_runtime::TypedTensorView; +use tenferro_tensor::TypedTensorViewMut; + +// Wrap a column-major faer::Mat without copying. faer pads columns to +// alignment, so the borrowed slice spans `col_stride * ncols` elements and the +// column stride is passed explicitly; the padding is never read logically. +let mat = faer::Mat::from_fn(2, 3, |r, c| (r * 3 + c) as f64); +let data = unsafe { std::slice::from_raw_parts(mat.as_ref().as_ptr(), (mat.col_stride() as usize) * mat.ncols()) }; +let view = TypedTensorView::from_slice(vec![2, 3], vec![1_isize, mat.col_stride()], 0, data)?; +assert_eq!(view.get(&[1, 2]), Some(&5.0)); + +// Wrap an ndarray row-major view the same way. Strides are arbitrary, so a +// row-major buffer is not transposed; it is only wrapped. +let arr = ndarray::Array2::from_shape_vec((2, 3), (0..6).map(|i| i as f64).collect())?; +let nview = TypedTensorView::from_slice(arr.shape(), arr.strides(), 0, arr.as_slice().expect("row-major array is contiguous"))?; +assert_eq!(nview.get(&[0, 2]), Some(&2.0)); + +// The mutable variant writes through the borrowed buffer. +let mut buffer = [0.0_f64, 0.0, 0.0, 0.0]; +let mut mview = TypedTensorViewMut::from_slice(vec![2, 2], vec![1, 2], 0, &mut buffer)?; +*mview.get_mut(&[0, 0]).expect("in-bounds index") = 7.0; +assert_eq!(buffer[0], 7.0); +``` + + +Because strides are arbitrary, row-major data wraps without transposition — +but kernels are tuned for column-major contiguity. Materialize a copy when the +wrapped buffer is row-major or its lifetime ends before the operation; +`TypedTensorView::duplicate()` requires a column-major-contiguous view, so for +a row-major wrap copy into a fresh `TypedTensor` (or an owned `Vec` in +column-major order) instead. If you search for the retired +`TypedStridedTensorView` (tenferro-rs#886), stop: `TypedTensorView::from_slice` +is its successor. diff --git a/.kimi/skills/tenferro-compute/references/pitfalls.md b/.kimi/skills/tenferro-compute/references/pitfalls.md index 29340efda..2482476cd 100644 --- a/.kimi/skills/tenferro-compute/references/pitfalls.md +++ b/.kimi/skills/tenferro-compute/references/pitfalls.md @@ -38,3 +38,48 @@ is in the [traced API example](api-cheatsheet.md#traced-tensors-and-extensions). `blas-accelerate`) when using `cpu-blas`. - CUDA is explicit: enable `tenferro-gpu`'s `cuda` feature and upload CPU tensors before CUDA operations. There is no implicit transfer. + +## Per-origin traps + +Same traps, keyed by the priors you arrived with. + +### If you come from ndarray / NumPy + +- Your buffers are row-major; passing them to `from_vec_col_major` silently + reinterprets them as column-major (no error, permuted/wrong values). Reorder + explicitly or wrap with + `TypedTensorView::from_slice` (see the [API cheatsheet](api-cheatsheet.md#borrowing-external-memory)). +- `cargo add tenferro` fails: there is no facade crate. Add + `tenferro-runtime` + `tenferro-cpu` (plus operation crates). +- `Array2::dot` is a free-standing habit; tenferro direct ops take an explicit + `&mut CpuBackend` argument: `a.matmul(&b, &mut backend)`. +- Your priors do not include a dtype-erased tensor; `Tensor` (runtime dtype) + has no ndarray counterpart — use `TypedTensor`. + +### If you come from nalgebra + +- nalgebra is column-major like tenferro, so `.data` / `as_slice()` buffers map + directly to `from_vec_col_major`. +- `Matrix::dot` / `gemm` are methods without an execution context; tenferro + direct ops need the explicit backend argument. Eager and traced tiers drop + it: `EagerTensor` methods run through the `EagerRuntime`, traced methods + build a graph. +- A single `DMatrix` maps to `TypedTensor`; there is no separate runtime + dtype type unless you actually need runtime dtype selection. + +### If you come from PyTorch / JAX + +- Trace one level of `?`: traced operators return `Result`; `a.matmul(&b)` is + a `Result`, unlike `torch.matmul` / `jnp.matmul`. +- Einsum needs the explicit arrow and rejects `...` (see + [Einsum syntax](#einsum-syntax)). +- The backend is not ambient: eager code must own an `EagerRuntime`, traced + code must register an engine plus extension modules (see + [Extension registration](#extension-registration)). + +## Retired names + +Searching for the retired `TypedStridedTensorView` (tenferro-rs#886) leads +nowhere: the name survives only in the obsolete-names vocabulary test. The +zero-copy view API is `TypedTensorView::from_slice` / `TypedTensorViewMut::from_slice` +(see the [API cheatsheet](api-cheatsheet.md#borrowing-external-memory)). diff --git a/README.md b/README.md index 2b7c7fd52..ad77f2eae 100644 --- a/README.md +++ b/README.md @@ -32,6 +32,22 @@ targets. ![tenferro-rs architecture overview](docs/assets/tenferro-architecture.svg) +## Conventions you must know before writing code + +- **Column-major storage.** Dense buffers are column-major: the leftmost + dimension varies fastest. Row-major data passed to `from_vec_col_major` is + silently reinterpreted as column-major — permuted/wrong values, never + rejected. +- **No facade crate.** `cargo add tenferro` fails by design; depend on the + crates you need (`tenferro-runtime`, `tenferro-cpu`, and operation crates). +- **Explicit backend.** Direct operations take an explicit backend argument; + construct the backend/runtime once and reuse it — per-call construction + discards the buffer pool. +- **Einsum dialect.** Equations need the explicit arrow (`"ij,jk->ik"`); `...` + ellipsis is unsupported. +- **Result-returning operators.** Traced operators return `Result`; propagate + with `?`. + ## Quickstart A: Direct Tensor Compute Add the runtime, CPU backend, and linear algebra extension crates: @@ -276,10 +292,18 @@ The full guides, tutorials, API reference, architecture notes, and specifications live at . Start with [Getting Started](https://tensor4all.org/tenferro-rs/getting-started/index.html); PyTorch/JAX users can also jump in through the -[PyTorch and JAX mapping](https://tensor4all.org/tenferro-rs/getting-started/pytorch-jax-mapping.html). +[PyTorch and JAX mapping](https://tensor4all.org/tenferro-rs/getting-started/pytorch-jax-mapping.html), +and Rust users coming from ndarray, nalgebra, or ndarray-linalg through the +[ndarray/nalgebra mapping](https://tensor4all.org/tenferro-rs/getting-started/ndarray-nalgebra-mapping.html). Agents and users writing downstream Rust should load the bundled [tenferro-compute skill](https://github.com/tensor4all/tenferro-rs/blob/main/.agents/skills/tenferro-compute/SKILL.md) -for API-tier, crate, import, and pitfall guidance. +for API-tier, crate, import, and pitfall guidance, or start from the +[llms.txt index](docs/llms.txt) — the single machine-oriented router whose +entries (including the skill's [API cheatsheet](https://tensor4all.org/tenferro-rs/skill-references/api-cheatsheet.md), +[crate selection](https://tensor4all.org/tenferro-rs/skill-references/crate-selection.md), +[performance idioms](https://tensor4all.org/tenferro-rs/skill-references/performance-idioms.md), +and [pitfalls](https://tensor4all.org/tenferro-rs/skill-references/pitfalls.md) +recipes) are one fetch away. Selected deep dives: diff --git a/docs/_quarto.yml b/docs/_quarto.yml index 057a60085..9f6ae073c 100644 --- a/docs/_quarto.yml +++ b/docs/_quarto.yml @@ -35,6 +35,7 @@ website: - getting-started/index.md - getting-started/core-concepts.md - getting-started/pytorch-jax-mapping.md + - getting-started/ndarray-nalgebra-mapping.md - section: "Tutorials" contents: - tutorials/index.md diff --git a/docs/getting-started/ndarray-nalgebra-mapping.md b/docs/getting-started/ndarray-nalgebra-mapping.md new file mode 100644 index 000000000..14de6b060 --- /dev/null +++ b/docs/getting-started/ndarray-nalgebra-mapping.md @@ -0,0 +1,179 @@ +# Coming from ndarray / nalgebra / ndarray-linalg + +This page is a translation guide for Rust users who arrive with `ndarray`, +`nalgebra`, or `ndarray-linalg` priors. PyTorch/JAX users have their own +[mapping page](./pytorch-jax-mapping.md); read +[Core Concepts](./core-concepts.md) for tenferro's own mental model. + +The short version is the five conventions every tenferro program must follow: + +- **Column-major storage.** Dense buffers are column-major: the leftmost + dimension varies fastest. Row-major data passed to `from_vec_col_major` is + silently reinterpreted as column-major — permuted/wrong values, never + rejected. +- **No facade crate.** `cargo add tenferro` fails by design; depend on the + crates you need (`tenferro-runtime`, `tenferro-cpu`, and operation crates). +- **Explicit backend.** Direct operations take an explicit backend argument; + construct the backend/runtime once and reuse it — per-call construction + discards the buffer pool. +- **Einsum dialect.** Equations need the explicit arrow (`"ij,jk->ik"`); `...` + ellipsis is unsupported. +- **Result-returning operators.** Traced operators return `Result`; propagate + with `?`. + +Each is explained below with the concrete ndarray/nalgebra divergence. + +## The column-major asymmetry + +| Prior | Storage order | `from_vec_col_major` hazard | +| --- | --- | --- | +| `ndarray` / NumPy | Row-major (C order) | **Dangerous**: a row-major flat buffer is silently reinterpreted as column-major (permuted/wrong values), never rejected | +| `nalgebra` | Column-major (F order) | Safe: `as_slice()` / `.data` map directly | + +This is the one prior that actively hurts. `ndarray` and NumPy store rows +contiguously, so the natural flat buffer you already have is in the wrong +physical order for tenferro — and construction does **not** detect the +mistake. A `[2, 3]` tenferro tensor reads elements down each column first. +Reorder the buffer explicitly before `from_vec_col_major`, or wrap it without +copying (see [Zero-copy interop](#zero-copy-interop-keep-your-faerndarray-buffers)). +`nalgebra` uses the same Fortran/column-major order as tenferro, LAPACK, and +Julia, so a `Matrix`'s `as_slice()` or `.data` maps directly into +`from_vec_col_major` with its shape. + +## Crate selection: `cargo add tenferro` does not exist + +There is deliberately no facade crate, so `cargo add tenferro` fails. Add the +smallest set of crates for your API tier directly: + +| Program | Minimum direct crates | +| --- | --- | +| Concrete `Tensor` / `TypedTensor` compute | `tenferro-runtime`, `tenferro-cpu` | +| Eager forward / AD | `tenferro-ad`, `tenferro-cpu` | +| Traced graph | `tenferro-runtime`, `tenferro-cpu` (+ `tenferro-ad` for graph transforms) | +| Linear algebra / einsum / FFT | add `tenferro-linalg` / `tenferro-einsum` / `tenferro-fft` | +| CUDA | the value/op crates plus `tenferro-gpu` with the `cuda` feature | + +See the [crate selection reference](https://tensor4all.org/tenferro-rs/skill-references/crate-selection.md) +for full dependency blocks and feature rules. + +## The backend is an explicit value + +In `ndarray`/`nalgebra`/`ndarray-linalg` the execution context is ambient: the +BLAS that was linked at build time is whatever your ops run through. tenferro +reifies it as a value — `CpuBackend` (or an `EagerRuntime`, or a `Runtime` for +traced graphs) — because the backend owns device placement, provider +selection, and buffer pools. + +The companion idiom is **construct once, reuse**. A backend is owned state: + +```rust +let mut backend = CpuBackend::new(); +// ... many operations through the same `backend` ... +``` + +Constructing `CpuBackend::new()` per call discards the buffer pool and cache +each time, and defeats reuse of compiled programs. The same rule applies +across the tiers — see the [performance idioms reference](https://tensor4all.org/tenferro-rs/skill-references/performance-idioms.md). + +## faer vs BLAS providers + +tenferro's CPU backend has two provider families controlled by additive +features: the `cpu-faer` provider and the `cpu-blas` provider. `ndarray-linalg` +users know feature-based BLAS selection; tenferro's knobs live in the +`tenferro-cpu` (and `tenferro-runtime`) features. + +| Provider | Features | When to use | +| --- | --- | --- | +| faer (default) | `cpu-faer` | Portable, pure Rust, no system dependencies; the right default for most workloads | +| BLAS / LAPACK | `cpu-blas` plus exactly one explicit provider feature | Large GEMM-dominated workloads, or to reuse an already-tuned system BLAS | + +`cpu-faer` and `cpu-blas` are additive, and `CpuBackend::new()` selects the +compiled default — BLAS when `cpu-blas` is compiled, otherwise faer — with +`CpuBackend::with_kind` for explicit selection when both are compiled. Within +the BLAS family the three explicit provider features (`blas-openblas`, +`blas-mkl`, `blas-accelerate`) are mutually exclusive, and tenferro rejects a +build that enables more than one. + +## Operation arity: `.dot()` to `matmul` + +The receiver-and-arity shape changes across the three tenferro tiers, and the +backends are pushed into method signatures. Compare with `ndarray`'s +`a.dot(&b)`: + +| Tier | Tenferro | Notes | +| --- | --- | --- | +| Direct | `a.matmul(&b, &mut backend)?` | explicit mutable backend argument | +| Eager | `a.matmul(&b)?` | `EagerRuntime` owns the backend | +| Traced | `a.matmul(&b)?` | builds a graph; returns `Result` | + + +```rust +use tenferro_cpu::CpuBackend; +use tenferro_runtime::{TypedTensor, TypedTensorOpsExt}; + +let mut backend = CpuBackend::new(); +// The leftmost dimension varies fastest: this is a 2 x 3 column-major tensor. +let x = TypedTensor::::from_vec_col_major( + vec![2, 3], + vec![1.0, 4.0, 2.0, 5.0, 3.0, 6.0], +)?; +let weights = TypedTensor::::from_vec_col_major( + vec![3, 2], + vec![0.5, -1.0, 1.5, 1.0, 2.0, -0.5], +)?; +let projected = x.matmul(&weights, &mut backend)?; +assert_eq!(projected.shape(), &[2, 2]); +assert_eq!(projected.host_data()?, &[3.0, 6.0, 3.5, 11.0]); +``` + + +In the direct tier the explicit backend moves from "linked somehow" to "passed +per call"; in the eager and traced tiers it is owned by the runtime and the +signature matches what `ndarray` users expect. + +## Tensor vs TypedTensor + +`ndarray`'s `Array` and `nalgebra`'s `Matrix` are generic over +the element type — your priors map to `TypedTensor`: + +| Your prior | Tenferro | +| --- | --- | +| `ndarray::Array2` | `TypedTensor` (rank-generic) | +| `nalgebra::DMatrix` | `TypedTensor` | +| dtype chosen at runtime | `Tensor` (dtype-erased, has no ndarray counterpart) | + +`Tensor` has no `ndarray`/`nalgebra` analogue: its element type is selected at +runtime. Reach for it when you need runtime dtype dispatch or direct backend +dispatch; for the ordinary fixed-element-type case the generic `TypedTensor` +is the direct match. + +## Zero-copy interop: keep your faer/ndarray buffers + +Adopting tenferro kernels does **not** require migrating tensor ownership. You +can wrap an existing column-major buffer — a `faer::Mat`, an `ndarray` view, or +any slice plus strides — zero-copy with `TypedTensorView::from_slice`, and +write through it with `TypedTensorViewMut::from_slice`: + +```text +your faer::Mat / ndarray view -> TypedTensorView::from_slice(shape, strides, offset, data) +``` + +The full runnable recipe (faer column padding, ndarray row-major wrap, and the +mutable variant) is in the [API cheatsheet "Borrowing external memory"](https://tensor4all.org/tenferro-rs/skill-references/api-cheatsheet.md#borrowing-external-memory). +Because strides are arbitrary, row-major data wraps **without** transposition — +but kernels are tuned for column-major contiguity, so materialize a copy when +performance matters and the wrapped buffer is row-major. + +The "do I have to own a new tensor type" objection is partly cost, and the +measured answer is small: adding tenferro on top of an existing `faer` +dependency costs about **+10 unique crates and +28 s of one-time cold build** +(see [tenferro-rs#1602](https://github.com/tensor4all/tenferro-rs/issues/1602)). +For a larger GEMM, the kernel-run portion is what you are adopting; the memory +stays yours. + +## Next steps + +- [Core Concepts](./core-concepts.md) — tenferro's own mental model. +- [Choosing a Tensor API](../guides/choosing-an-api.md) — `TypedTensor`, `Tensor`, `EagerTensor`, or `TracedTensor`. +- [PyTorch and JAX Mapping](./pytorch-jax-mapping.md) — the same translation for torch/JAX priors. +- [API cheatsheet](https://tensor4all.org/tenferro-rs/skill-references/api-cheatsheet.md) — tier-by-tier arities and recipes. diff --git a/docs/index.md b/docs/index.md index bba3b42a8..5a9d74011 100644 --- a/docs/index.md +++ b/docs/index.md @@ -17,6 +17,7 @@ compilation, CUDA, or experimental WebGPU only when the workflow needs them. | Workflow | Start with | | --- | --- | | First setup and a checked CPU program | [Getting Started](getting-started/index.md) | +| Coming from ndarray, nalgebra, or ndarray-linalg | [ndarray / nalgebra Mapping](getting-started/ndarray-nalgebra-mapping.md) | | Core terms and the three main choices | [Core Concepts](getting-started/core-concepts.md) | | Step-by-step runnable examples | [Tutorials](tutorials/index.md) | | Choosing between `TypedTensor`, `Tensor`, `EagerTensor`, and `TracedTensor` | [Choosing a Tensor API](guides/choosing-an-api.md) | diff --git a/docs/llms.txt b/docs/llms.txt index 969afa0fc..352f1c0b9 100644 --- a/docs/llms.txt +++ b/docs/llms.txt @@ -2,19 +2,36 @@ > tenferro is a Rust-native tensor computation stack with opt-in autodiff for scientific workloads: typed tensors, PyTorch-style eager execution, JAX-style traced graphs, einsum, linear algebra, and explicit CPU, CUDA, and experimental WebGPU backend control. +## Conventions you must know before writing code + +- **Column-major storage.** Dense buffers are column-major: the leftmost dimension varies fastest. Row-major data passed to `from_vec_col_major` is silently reinterpreted as column-major — permuted/wrong values, never rejected. +- **No facade crate.** `cargo add tenferro` fails by design; depend on the crates you need (`tenferro-runtime`, `tenferro-cpu`, and operation crates). +- **Explicit backend.** Direct operations take an explicit backend argument; construct the backend/runtime once and reuse it — per-call construction discards the buffer pool. +- **Einsum dialect.** Equations need the explicit arrow (`"ij,jk->ik"`); `...` ellipsis is unsupported. +- **Result-returning operators.** Traced operators return `Result`; propagate with `?`. + +## Recipes and references + +- [README](https://github.com/tensor4all/tenferro-rs/blob/main/README.md): The canonical router — every user-facing artifact (guides, recipes, rustdoc, and this index) is reachable from it. +- [API cheatsheet](https://tensor4all.org/tenferro-rs/skill-references/api-cheatsheet.md): Tier-by-tier method arities and extension-trait imports, including the zero-copy recipe for wrapping external faer/ndarray memory. +- [Crate selection](https://tensor4all.org/tenferro-rs/skill-references/crate-selection.md): Which crates to add for each API tier, and CPU provider feature rules. +- [Performance idioms](https://tensor4all.org/tenferro-rs/skill-references/performance-idioms.md): Construct-once-reuse and compile-once/run-many execution patterns. +- [Pitfalls](https://tensor4all.org/tenferro-rs/skill-references/pitfalls.md): Column-major input, einsum syntax, extension registration, and per-origin traps for ndarray/nalgebra/PyTorch arrivals. + ## Getting started - [Getting Started](https://tensor4all.org/tenferro-rs/getting-started/): Walks through tenferro's tensor layer, execution model, and backend/device choices, including a first CPU program. +- [Coming from ndarray / nalgebra / ndarray-linalg](https://tensor4all.org/tenferro-rs/getting-started/ndarray-nalgebra-mapping.html): For Rust users with ndarray or nalgebra priors: the column-major asymmetry, crate selection, explicit backends, faer vs BLAS providers, and zero-copy interop with existing buffers. - [PyTorch and JAX Mapping](https://tensor4all.org/tenferro-rs/getting-started/pytorch-jax-mapping.html): Translates common `torch` and `jax.numpy` operations into tenferro's direct, eager, and traced APIs. - [Choosing a Tensor API](https://tensor4all.org/tenferro-rs/guides/choosing-an-api.html): Explains how to select the tensor API by value type, execution timing, and backend/device. ## Guides -- [Einsum](https://tensor4all.org/tenferro-rs/guides/einsum.html): Covers the `tenferro-einsum` extension crate, its concrete/eager/traced traits, planning, syntax, and runtime registration. +- [Einsum](https://tensor4all.org/tenferro-rs/guides/einsum.html): WARNING — tenferro's dialect requires the explicit arrow and rejects `...` ellipsis; read before porting an equation. - [Linear Algebra](https://tensor4all.org/tenferro-rs/guides/linear-algebra.html): Covers the `tenferro-linalg` operation crate and its direct, eager, and traced execution surfaces. - [Autodiff](https://tensor4all.org/tenferro-rs/guides/autodiff.html): Describes eager `backward()` and functional or traced `grad`, `vjp`, and `jvp` workflows. -- [Parallelism and Caching](https://tensor4all.org/tenferro-rs/guides/parallelism-and-caching.html): Describes CPU thread control, provider oversubscription, executor reuse, and cache management. -- [Memory Order](https://tensor4all.org/tenferro-rs/guides/memory-order.html): Specifies contiguous column-major storage and leftmost-dimension-fastest layout. +- [Parallelism and Caching](https://tensor4all.org/tenferro-rs/guides/parallelism-and-caching.html): WARNING — backend/runtime instances own thread pools and caches; per-call construction throws them away. +- [Memory Order](https://tensor4all.org/tenferro-rs/guides/memory-order.html): WARNING — buffers are column-major; row-major data passed to `from_vec_col_major` is silently reinterpreted as column-major (wrong values), never rejected. - [Devices and GPU](https://tensor4all.org/tenferro-rs/guides/devices-and-gpu.html): Documents explicit CPU/GPU placement and transfer boundaries plus CUDA and WebGPU capability coverage. - [Troubleshooting](https://tensor4all.org/tenferro-rs/guides/troubleshooting.html): Covers feature selection, BLAS providers, CUDA setup, workspace traps, and common runtime failures. diff --git a/docs/tutorial-code/Cargo.toml b/docs/tutorial-code/Cargo.toml index a2fdbaf67..c2a0d381c 100644 --- a/docs/tutorial-code/Cargo.toml +++ b/docs/tutorial-code/Cargo.toml @@ -26,6 +26,8 @@ cuda = [ ] [dependencies] +faer.workspace = true +ndarray = "0.16" num-complex.workspace = true tenferro-ad = { path = "../../crates/tenferro-ad", default-features = false } tenferro-cpu = { path = "../../crates/tenferro-cpu", default-features = false } diff --git a/docs/tutorial-code/src/bin/tenferro_compute_skill.rs b/docs/tutorial-code/src/bin/tenferro_compute_skill.rs index 49382168b..b4f0d9e6e 100644 --- a/docs/tutorial-code/src/bin/tenferro_compute_skill.rs +++ b/docs/tutorial-code/src/bin/tenferro_compute_skill.rs @@ -116,10 +116,40 @@ for (input, expected) in [ Ok(()) } +#[rustfmt::skip] +fn borrowing_external_memory() -> Result<(), Box> { + // snippet-start:borrowing-external-memory +use tenferro_runtime::TypedTensorView; +use tenferro_tensor::TypedTensorViewMut; + +// Wrap a column-major faer::Mat without copying. faer pads columns to +// alignment, so the borrowed slice spans `col_stride * ncols` elements and the +// column stride is passed explicitly; the padding is never read logically. +let mat = faer::Mat::from_fn(2, 3, |r, c| (r * 3 + c) as f64); +let data = unsafe { std::slice::from_raw_parts(mat.as_ref().as_ptr(), (mat.col_stride() as usize) * mat.ncols()) }; +let view = TypedTensorView::from_slice(vec![2, 3], vec![1_isize, mat.col_stride()], 0, data)?; +assert_eq!(view.get(&[1, 2]), Some(&5.0)); + +// Wrap an ndarray row-major view the same way. Strides are arbitrary, so a +// row-major buffer is not transposed; it is only wrapped. +let arr = ndarray::Array2::from_shape_vec((2, 3), (0..6).map(|i| i as f64).collect())?; +let nview = TypedTensorView::from_slice(arr.shape(), arr.strides(), 0, arr.as_slice().expect("row-major array is contiguous"))?; +assert_eq!(nview.get(&[0, 2]), Some(&2.0)); + +// The mutable variant writes through the borrowed buffer. +let mut buffer = [0.0_f64, 0.0, 0.0, 0.0]; +let mut mview = TypedTensorViewMut::from_slice(vec![2, 2], vec![1, 2], 0, &mut buffer)?; +*mview.get_mut(&[0, 0]).expect("in-bounds index") = 7.0; +assert_eq!(buffer[0], 7.0); + // snippet-end:borrowing-external-memory + Ok(()) +} + fn main() -> Result<(), Box> { concrete_operation_and_column_major()?; eager_operation()?; traced_operation_with_extension_registration()?; compile_once_run_many()?; + borrowing_external_memory()?; Ok(()) } diff --git a/scripts/build_docs_site.sh b/scripts/build_docs_site.sh index 2a87bfd4d..86d548ad4 100755 --- a/scripts/build_docs_site.sh +++ b/scripts/build_docs_site.sh @@ -12,6 +12,11 @@ rm -rf "$OUT_DIR" mkdir -p "$API_DIR" cp "$ROOT_DIR/docs/llms.txt" "$OUT_DIR/llms.txt" +# Republish the skill's snippet-verified references so llms.txt recipes are one +# fetch away; the site copies are byte-identical to the canonical skill files. +mkdir -p "$OUT_DIR/skill-references" +cp "$ROOT_DIR/.agents/skills/tenferro-compute/references/"*.md "$OUT_DIR/skill-references/" + render_overview_html() { if [[ -f "$API_INDEX_MD" ]]; then if command -v pandoc >/dev/null 2>&1; then diff --git a/scripts/check-docs-site.py b/scripts/check-docs-site.py index 33b5ce78d..81f89c2a8 100644 --- a/scripts/check-docs-site.py +++ b/scripts/check-docs-site.py @@ -19,6 +19,8 @@ LLMS_SITE_PREFIX = "/tenferro-rs/" LLMS_LINK_RE = re.compile(r"^\s*-\s+\[([^]]+)\]\(([^)]+)\):\s*(\S.*)$", re.MULTILINE) LLMS_SKILL_PATH = ".agents/skills/tenferro-compute/SKILL.md" +LLMS_SKILL_REFERENCES_PREFIX = "skill-references/" +LLMS_README_URL = "https://github.com/tensor4all/tenferro-rs/blob/main/README.md" class LinkCollector(HTMLParser): @@ -101,6 +103,13 @@ def llms_source_path(root: pathlib.Path, url: str) -> pathlib.Path | None: parsed = urlsplit(url) if parsed.netloc == "tensor4all.org" and parsed.path.startswith(LLMS_SITE_PREFIX): relative = unquote(parsed.path[len(LLMS_SITE_PREFIX) :]) + # Republished skill references are copied into the site build from the + # canonical skill; resolve them back to that single source of truth. + if relative.startswith(LLMS_SKILL_REFERENCES_PREFIX): + name = relative[len(LLMS_SKILL_REFERENCES_PREFIX) :] + if not name.endswith(".md"): + return None + return root / ".agents" / "skills" / "tenferro-compute" / "references" / name if relative.endswith("/"): relative += "index.md" elif relative.endswith(".html"): @@ -114,6 +123,46 @@ def llms_source_path(root: pathlib.Path, url: str) -> pathlib.Path | None: return None +def llms_skill_reference_names(text: str) -> set[str]: + names: set[str] = set() + for match in LLMS_LINK_RE.finditer(text): + url = match.group(2) + parsed = urlsplit(url) + if parsed.netloc != "tensor4all.org": + continue + relative = unquote(parsed.path) + if not relative.startswith(LLMS_SITE_PREFIX + LLMS_SKILL_REFERENCES_PREFIX): + continue + name = relative[len(LLMS_SITE_PREFIX + LLMS_SKILL_REFERENCES_PREFIX) :] + if name.endswith(".md"): + names.add(name) + return names + + +def markdown_link_targets(text: str) -> set[str]: + return {target for target in re.findall(r"\[[^]]*\]\(([^)]+)\)", text)} + + +def check_reachability(root: pathlib.Path) -> list[str]: + """Assert the README is the single router and llms.txt links back to it.""" + errors: list[str] = [] + readme = root / "README.md" + if not readme.is_file(): + return ["README.md is missing; router reachability cannot be checked"] + readme_targets = markdown_link_targets(readme.read_text(encoding="utf-8")) + if not any(target == "docs/llms.txt" or target.endswith("/docs/llms.txt") for target in readme_targets): + errors.append("README.md must link docs/llms.txt (single router)") + if not any(LLMS_SKILL_PATH in target for target in readme_targets): + errors.append(f"README.md must link {LLMS_SKILL_PATH}") + llms_index = root / "docs" / "llms.txt" + llms_targets = ( + markdown_link_targets(llms_index.read_text(encoding="utf-8")) if llms_index.is_file() else set() + ) + if LLMS_README_URL not in llms_targets: + errors.append(f"docs/llms.txt must link back to the README ({LLMS_README_URL})") + return errors + + def check_llms_index(root: pathlib.Path, docs_site_root: pathlib.Path | None = None) -> list[str]: index = root / "docs" / "llms.txt" if not index.is_file(): @@ -145,8 +194,13 @@ def check_llms_index(root: pathlib.Path, docs_site_root: pathlib.Path | None = N errors.append(f"docs/llms.txt must link {LLMS_SKILL_PATH}") elif not (root / LLMS_SKILL_PATH).is_file(): errors.append(f"docs/llms.txt skill target does not exist: {LLMS_SKILL_PATH}") - if docs_site_root is not None and docs_site_root.exists() and not (docs_site_root / "llms.txt").is_file(): - errors.append(f"built docs site is missing root llms.txt: {docs_site_root / 'llms.txt'}") + if docs_site_root is not None and docs_site_root.exists(): + if not (docs_site_root / "llms.txt").is_file(): + errors.append(f"built docs site is missing root llms.txt: {docs_site_root / 'llms.txt'}") + for name in sorted(llms_skill_reference_names(index.read_text(encoding="utf-8"))): + built = docs_site_root / LLMS_SKILL_REFERENCES_PREFIX / name + if not built.is_file(): + errors.append(f"built docs site is missing republished skill reference: {built}") return errors @@ -326,6 +380,13 @@ def main() -> int: print(f"- {error}", file=sys.stderr) return 1 + reachability_errors = check_reachability(root) + if reachability_errors: + print("docs reachability failed:", file=sys.stderr) + for error in reachability_errors: + print(f"- {error}", file=sys.stderr) + return 1 + crates = load_workspace_libs(root) missing_doc = [pkg for _member, pkg, doc_dir in crates if not (doc_root / doc_dir / "index.html").exists()] if missing_doc: diff --git a/scripts/test-check-docs-site.py b/scripts/test-check-docs-site.py index 8c33a9589..9a4c051bf 100644 --- a/scripts/test-check-docs-site.py +++ b/scripts/test-check-docs-site.py @@ -17,6 +17,15 @@ def write(path: pathlib.Path, text: str) -> None: def make_minimal_docs_root(root: pathlib.Path) -> None: + write( + root / "README.md", + """ + # demo + + Read [llms.txt](docs/llms.txt) and the + [skill](.agents/skills/tenferro-compute/SKILL.md). + """, + ) write( root / "Cargo.toml", """ @@ -71,6 +80,7 @@ def make_minimal_docs_root(root: pathlib.Path) -> None: write( root / "docs/llms.txt", """ + - [README](https://github.com/tensor4all/tenferro-rs/blob/main/README.md): Router. - [Extension](https://tensor4all.org/tenferro-rs/spec/extension-op.html): The extension specification. - [Skill](https://github.com/tensor4all/tenferro-rs/blob/main/.agents/skills/tenferro-compute/SKILL.md): Downstream usage guidance. """, @@ -114,6 +124,7 @@ def test_llms_missing_target_fails() -> None: write( fake_root / "docs/llms.txt", "- [Missing](https://tensor4all.org/tenferro-rs/spec/missing.html): Not present.\n" + "- [README](https://github.com/tensor4all/tenferro-rs/blob/main/README.md): Router.\n" "- [Skill](https://github.com/tensor4all/tenferro-rs/blob/main/.agents/skills/tenferro-compute/SKILL.md): Skill.\n", ) result = run_checker(fake_root) @@ -139,6 +150,7 @@ def test_llms_duplicate_url_fails() -> None: fake_root / "docs/llms.txt", "- [One](https://tensor4all.org/tenferro-rs/spec/extension-op.html): One.\n" "- [Two](https://tensor4all.org/tenferro-rs/spec/extension-op.html): Two.\n" + "- [README](https://github.com/tensor4all/tenferro-rs/blob/main/README.md): Router.\n" "- [Skill](https://github.com/tensor4all/tenferro-rs/blob/main/.agents/skills/tenferro-compute/SKILL.md): Skill.\n", ) result = run_checker(fake_root) @@ -146,8 +158,137 @@ def test_llms_duplicate_url_fails() -> None: assert "repeats URL" in result.stderr +def test_readme_missing_llms_link_fails() -> None: + with tempfile.TemporaryDirectory() as tmp: + fake_root = pathlib.Path(tmp) + make_minimal_docs_root(fake_root) + write( + fake_root / "README.md", + "# demo\n\nRead the [skill](.agents/skills/tenferro-compute/SKILL.md).\n", + ) + result = run_checker(fake_root) + assert result.returncode != 0 + assert "README.md must link docs/llms.txt" in result.stderr + + +def test_readme_plain_text_mention_is_not_a_link() -> None: + with tempfile.TemporaryDirectory() as tmp: + fake_root = pathlib.Path(tmp) + make_minimal_docs_root(fake_root) + # `docs/llms.txt` appears only in prose, not as a link target. + write( + fake_root / "README.md", + "# demo\n\nSee docs/llms.txt for the index and the\n" + "[skill](.agents/skills/tenferro-compute/SKILL.md).\n", + ) + result = run_checker(fake_root) + assert result.returncode != 0 + assert "README.md must link docs/llms.txt" in result.stderr + + +def test_readme_missing_skill_link_fails() -> None: + with tempfile.TemporaryDirectory() as tmp: + fake_root = pathlib.Path(tmp) + make_minimal_docs_root(fake_root) + # The skill path appears only in prose, not as a link target. + write( + fake_root / "README.md", + "# demo\n\nRead the [llms index](docs/llms.txt); the " + ".agents/skills/tenferro-compute/SKILL.md is also bundled.\n", + ) + result = run_checker(fake_root) + assert result.returncode != 0 + assert "README.md must link .agents/skills/tenferro-compute/SKILL.md" in result.stderr + + +def test_llms_missing_readme_link_fails() -> None: + with tempfile.TemporaryDirectory() as tmp: + fake_root = pathlib.Path(tmp) + make_minimal_docs_root(fake_root) + write( + fake_root / "docs/llms.txt", + "- [Skill](https://github.com/tensor4all/tenferro-rs/blob/main/.agents/skills/tenferro-compute/SKILL.md): Skill.\n", + ) + result = run_checker(fake_root) + assert result.returncode != 0 + assert "llms.txt must link back to the README" in result.stderr + + +def make_eager_ad_fixture(root: pathlib.Path) -> None: + """Create the sources read by check_eager_functional_ad_docs in the checker.""" + write(root / "docs/index.md", "# Home\n\n`EagerRuntime` functional `grad`, `vjp`, and `jvp`\n") + write( + root / "docs/getting-started/index.md", + "functional eager `grad`, `vjp`, and `jvp`\n", + ) + write( + root / "docs/getting-started/core-concepts.md", + "functional `grad`, `vjp`, and `jvp` transforms\n", + ) + write( + root / "docs/getting-started/pytorch-jax-mapping.md", + "`EagerRuntime` functional `grad`/`vjp`/`jvp`\n", + ) + write(root / "docs/tutorials/index.md", "functional eager AD entry point\n") + write( + root / "docs/spec/operation-categories.md", + "stateful `backward()` plus functional `grad`/`vjp`/`jvp`\n", + ) + write( + root / "docs/guides/eager-operations.md", + "stateful reverse-mode and functional `grad`/`vjp`/`jvp`\n", + ) + write( + root / "docs/assets/tenferro-architecture.svg", + "backward · grad and vjp · jvp\n", + ) + write( + root / "README.md", + "# demo\n\nBoth eager and traced modes support VJP and JVP, " + "and HVP-style higher-order composition.\n" + "Read [llms.txt](docs/llms.txt) and the\n" + "[skill](.agents/skills/tenferro-compute/SKILL.md).\n", + ) + + +def test_skill_reference_resolves_and_built_site_copy_checked() -> None: + with tempfile.TemporaryDirectory() as tmp: + fake_root = pathlib.Path(tmp) + make_minimal_docs_root(fake_root) + write( + fake_root / ".agents/skills/tenferro-compute/references/api-cheatsheet.md", + "# API cheatsheet\n", + ) + write( + fake_root / "docs/llms.txt", + "- [README](https://github.com/tensor4all/tenferro-rs/blob/main/README.md): Router.\n" + "- [API cheatsheet](https://tensor4all.org/tenferro-rs/skill-references/api-cheatsheet.md): Recipe.\n" + "- [Skill](https://github.com/tensor4all/tenferro-rs/blob/main/.agents/skills/tenferro-compute/SKILL.md): Skill.\n", + ) + # The source exists but the built-site copy is missing: must fail. + result = run_checker(fake_root) + assert result.returncode != 0 + assert "missing republished skill reference" in result.stderr + # Once the build copies it, the same index passes. + write(fake_root / "target/docs-site/skill-references/api-cheatsheet.md", "# API cheatsheet\n") + # Resolve the intentionally-broken design link from the minimal root so + # the rendered-link check passes and the skill-reference check is the + # deciding gate for the positive case. + write(fake_root / "target/docs-site/design/dynamic-symbolic-shapes.html", "# design\n") + # The checker's eager-functional-AD docs audit reads many sources after + # the llms/reachability checks; provide them so the run can complete. + make_eager_ad_fixture(fake_root) + result = run_checker(fake_root) + assert result.returncode == 0, result.stderr + + if __name__ == "__main__": test_rendered_internal_link_outside_render_set_fails() test_llms_missing_target_fails() test_built_llms_missing_fails() test_llms_duplicate_url_fails() + test_readme_missing_llms_link_fails() + test_readme_plain_text_mention_is_not_a_link() + test_readme_missing_skill_link_fails() + test_llms_missing_readme_link_fails() + test_skill_reference_resolves_and_built_site_copy_checked()