跳轉到

Signed-Stride mdspan

Kernels should consume coordinates, not reimplement NumPy layout rules. std::mdspan provides that interface, but std::layout_stride::mapping requires positive supplied strides. Solvcon keeps mdspan and its accessor, and replaces only the mapping that connects coordinates to offsets.

The standard gap

std layout_stride requires positive supplied strides, while the reversed SimpleArray view needs strides negative three and negative one
The custom component is LayoutPolicy::mapping, not mdspan or its accessor.

A custom mapping must still return a nonnegative total offset because default_accessor receives that offset as size_t.

The custom mapping in code

SignedStrideLayout keeps the signed per-axis strides. During construction it finds the lowest and highest raw offsets and records the translation needed to make every mapping result nonnegative:

signed_stride_layout.hpp
constexpr void SignedStrideLayout::mapping<Extents>::initialize_offsets() noexcept
{
    index_type min_offset = 0;
    index_type max_offset = 0;

    for (rank_type rank = 0; rank < extents_type::rank(); ++rank)
    {
        if (m_extents.extent(rank) == 0)
        {
            m_origin_offset = 0;
            m_required_span_size = 0;
            return;
        }

        index_type const axis_offset =
            (m_extents.extent(rank) - 1) * m_strides[rank];
        if (axis_offset < 0)
        {
            min_offset += axis_offset;
        }
        else
        {
            max_offset += axis_offset;
        }
    }

    m_origin_offset = -min_offset;
    m_required_span_size = max_offset - min_offset + 1;
}

Each axis is linear, so its extrema occur at index 0 or extent-1. The loop sums those independent endpoint contributions.

The mapping applies the saved translation on every access:

signed_stride_layout.hpp
index_type offset = m_origin_offset;
rank_type rank = 0;
((offset += static_cast<index_type>(indices) * m_strides[rank++]), ...);
return offset;

This implements one mapping equation:

\[ \operatorname{map}(i)=\operatorname{origin\_offset} + \sum_d i_d\operatorname{stride}[d]. \]
SignedStrideLayout derives an origin offset of five from raw offsets negative five through zero and normalizes the mdspan mapping to zero through five
Signed strides remain signed; only the final mapping offset is translated.

For shape (2,3) and strides (-3,-1), the two axis endpoints contribute -3 and -2. Therefore min=-5, origin_offset=5, and the descriptor endpoints (0,0) and (1,2) map to 5 and 0.

Rebasing the data handle

Translating the mapping alone would change physical addresses. as_mdspan() therefore moves the handle in the opposite direction:

SimpleArray.hpp
auto const mapping = make_mdspan_mapping<N>();
value_type * data_handle = logical_data();
if (data_handle)
{
    data_handle -= mapping.origin_offset();
}
return std::mdspan<value_type,
                   std::dextents<ssize_t, N>,
                   detail::SignedStrideLayout>(data_handle, mapping);

The two translations cancel:

\[ \begin{aligned} \operatorname{handle}+\operatorname{map}(i) &=(\operatorname{logical\_data}-o)+(o+\operatorname{raw}(i)) \\ &=\operatorname{logical\_data}+\operatorname{raw}(i), \end{aligned} \]

where o is origin_offset. The adjusted handle is the lowest reachable address. It need not equal data() if a general ConcreteBuffer contains an unused prefix.

C++
using Array = solvcon::SimpleArray<double>;
auto buffer = solvcon::ConcreteBuffer::construct(6 * sizeof(double));
Array a(
    Array::shape_type{2, 3},
    Array::shape_type{-3, -1},
    buffer,
    5 * sizeof(double));

auto view = a.as_mdspan<2>();
// &view[0, 0] == a.logical_data()
// &view[1, 2] == a.data()

Contracts and limitations

  • Shape storage is signed, but each shape value must remain nonnegative.
  • The required span is the envelope of reachable addresses and may contain holes.
  • An empty extent produces a zero-size required span.
  • The mapping reports uniqueness, but runtime code does not reject zero or overlapping strides. The caller must provide a unique, representable mapping.
  • Offset arithmetic has no overflow check, and element access has no bounds check.
  • as_mdspan<N>() checks the runtime rank but does not own the array or buffer.
  • nghost is not mdspan metadata. The body begins at mdspan index nghost on axis 0.

Python assignment does not use this mdspan path. The next page translates a Python key into storage coordinates, then follows the same descriptor strides.

Sources