Skip to content

Commit 2b64bf1

Browse files
committed
review: hoist the ld bound out of the segment loops; build the blas unit test
tests/math_blas.cpp is tracked but was never listed in ta_test_src_files, so nothing in it has ever been compiled -- including the measured_ld_fits case added for this guard. Registering it next to linalg.cpp brings the whole blas_suite (17 cases) into the run; it passes as it stands. ld_fits() divides, and the two ce+ce walkers called it twice per admitted cell in the innermost segment-growth loop, whose other work is a handful of loads and compares. The strides and extents are fixed for a segment once off == 1, so take the bound once there instead: max_ld_offset(ld, extent) is the same inequality solved for the slab offset, and admission then costs a compare. The long arena-contiguous segments this kernel exists to hit -- g_seg_len9p_ce_ce -- were paying two divisions per cell for a check that can never fire on them. ld_fits() is now expressed through max_ld_offset(), so the two cannot drift apart; measure_segments takes the same hoist, since it exists to simulate the walker's segmentation exactly. Also, per review: - spell math::blas::ld_fits at the call sites rather than pulling it into TiledArray::detail with a using-declaration: arena_einsum.h is widely included, and the name is of no interest to anything else in detail. The rationale it carried becomes a section comment. - ce_e_stride_past_blas_int_falls_back returned before any assertion on both its skip paths (ILP64, refused reservation), so on those configurations it passed while checking nothing and only Boost's easily-missed "did not check any assertions" note said so. It now asserts that an ordinary contiguous 2-cell run is still accepted before either branch, and the ILP64 skip explains itself like the reservation one already did. - the reservation and the two placement-new'd cells get one RAII owner: a failing BOOST_REQUIRE skipped ~Cell(), leaking each cell's TA::Range buffer, which under this repo's ASan debug build would bury the real failure in leak reports. - drop two lambda captures of constants (`len`, `K`) that -Wall reports as unnecessary. CI runs TA_WERROR=ON without -Wall so they did not gate it, but cmake/toolchains/travis.cmake does add -Wall, and that combination makes them errors.
1 parent ec3227d commit 2b64bf1

4 files changed

Lines changed: 97 additions & 45 deletions

File tree

‎src/TiledArray/math/blas.h‎

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -61,19 +61,30 @@ inline constexpr integer max_ld() {
6161
return static_cast<integer>(std::numeric_limits<blas_int>::max());
6262
}
6363

64+
/// The largest row offset `o` for which element o*ld + extent-1 of a
65+
/// row-major matrix at leading dimension `ld` is still indexable by a
66+
/// `blas_int`, i.e. the largest `o` with ld_fits(ld, o+1, extent). Phrased as
67+
/// a division so the bound itself cannot overflow when `blas_int` is 64-bit
68+
/// (ILP64), where max_ld() is INT64_MAX. Callers that grow a run one row at a
69+
/// time should take this bound ONCE, while `ld` and `extent` are fixed,
70+
/// rather than calling ld_fits() per row -- that divides every time round the
71+
/// loop.
72+
inline constexpr integer max_ld_offset(integer ld, integer extent) {
73+
if (ld <= 0 || extent < 1 || extent - 1 > max_ld()) return 0;
74+
return (max_ld() - (extent - 1)) / ld;
75+
}
76+
6477
/// True if a row-major matrix of `nslab` rows and `extent` columns at leading
6578
/// dimension `ld` addresses only elements a `blas_int` can index. max_ld()
6679
/// alone bounds ONE row step; the GEMM reaches element
6780
/// (nslab-1)*ld + extent-1, and a 32-bit-indexed BLAS must be able to form
6881
/// THAT index too, so a stride comfortably under the cap still overflows it
69-
/// once there are enough rows. Phrased as a division so the bound itself
70-
/// cannot overflow when `blas_int` is 64-bit (ILP64), where max_ld() is
71-
/// INT64_MAX.
82+
/// once there are enough rows.
7283
inline constexpr bool ld_fits(integer ld, integer nslab, integer extent) {
7384
if (ld < 0 || extent < 1 || extent - 1 > max_ld() || ld > max_ld())
7485
return false;
7586
if (nslab <= 1) return true;
76-
return ld <= (max_ld() - (extent - 1)) / (nslab - 1);
87+
return nslab - 1 <= max_ld_offset(ld, extent);
7788
}
7889

7990
/// converts Op to ints in manner useful for bit manipulations

‎src/TiledArray/tensor/arena_einsum.h‎

Lines changed: 48 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -294,20 +294,27 @@ inline std::atomic<std::uint64_t> g_fall_op_acrossk_ce_ce{0}; // 16
294294
inline std::atomic<std::uint64_t> g_fall_both_clean_ce_ce{0}; // 17
295295
inline std::atomic<std::uint64_t> g_fall_op_ldrange_ce_ce{0}; // 18
296296

297-
/// Does a measured strided run address only elements the BLAS can index?
298-
/// The strided kernels below MEASURE their leading dimensions as pointer
299-
/// differences between neighbouring cells, so -- unlike a kernel that derives
300-
/// them from extents -- they can hand blaspp an ld it cannot convert to
301-
/// `blas_int` (`blas::Error: ldb, in function to_blas_int_`). Two present
302-
/// cells that are not arena neighbours -- e.g. individually allocated inner
303-
/// tensors the allocator placed gigabytes apart -- satisfy every other
304-
/// precondition of a 2-cell run (uniform size, stride >= cell size, trivially
305-
/// constant) yet yield exactly such an address delta; and a stride well inside
306-
/// `blas_int` still overflows the BLAS index once the run is long enough,
307-
/// because the GEMM reaches element (nslab-1)*ld + extent-1. Such a run is not
308-
/// strided-GEMM material: the walkers break it up and the per-cell path
309-
/// handles it.
310-
using math::blas::ld_fits;
297+
// ---------------------------------------------------------------------------
298+
// Measured leading dimensions vs. the BLAS index range.
299+
//
300+
// The strided kernels below MEASURE their leading dimensions as pointer
301+
// differences between neighbouring cells, so -- unlike a kernel that derives
302+
// them from extents -- they can hand blaspp an ld it cannot convert to
303+
// `blas_int` (`blas::Error: ldb, in function to_blas_int_`). Two present
304+
// cells that are not arena neighbours -- e.g. individually allocated inner
305+
// tensors the allocator placed gigabytes apart -- satisfy every other
306+
// precondition of a 2-cell run (uniform size, stride >= cell size, trivially
307+
// constant) yet yield exactly such an address delta; and a stride well inside
308+
// `blas_int` still overflows the BLAS index once the run is long enough,
309+
// because the GEMM reaches element (nslab-1)*ld + extent-1. Such a run is not
310+
// strided-GEMM material: the walkers break it up and the per-cell path
311+
// handles it.
312+
//
313+
// math::blas::ld_fits() is the whole-run test. The segment walkers instead
314+
// take math::blas::max_ld_offset() once per segment -- the same bound solved
315+
// for the slab offset -- because ld_fits() divides, and the admission loop
316+
// runs once per cell.
317+
// ---------------------------------------------------------------------------
311318

312319
/// Classify a strided run: 1=absent, 2=nonuniform size, 3=bad stride,
313320
/// 4=stride past the BLAS index range (see ld_fits), 0=clean.
@@ -334,7 +341,7 @@ inline int classify_run(GetCell getcell, std::size_t n) {
334341
if (getcell(i).data() != base + static_cast<std::ptrdiff_t>(i) * st)
335342
return 3; // non-constant stride
336343
// ...and the whole run must stay inside the BLAS index range
337-
if (!ld_fits(st, static_cast<long>(n), s0)) return 4;
344+
if (!math::blas::ld_fits(st, static_cast<long>(n), s0)) return 4;
338345
return 0;
339346
}
340347

@@ -375,7 +382,7 @@ inline int classify_operand(GetR getR, GetL getL, std::size_t nrun,
375382
for (std::size_t i = 0; i < nrun; ++i)
376383
if (getR(k, i).data() != base + static_cast<std::ptrdiff_t>(i) * sk)
377384
return 15;
378-
if (!ld_fits(sk, static_cast<long>(nrun), Q)) return 18;
385+
if (!math::blas::ld_fits(sk, static_cast<long>(nrun), Q)) return 18;
379386
if (k == 0)
380387
sR = sk;
381388
else if (sk != sR)
@@ -513,6 +520,7 @@ inline void measure_segments(GetC getC, GetR getR, GetL getL, std::size_t nrun,
513520
const auto* rb = r0.data();
514521
std::size_t end = mu + 1;
515522
long sC = -1, sR = -1;
523+
long max_off = 0; // set with the strides, below
516524
while (end < nrun) {
517525
const auto& ce = getC(end);
518526
const auto& re = getR(k, end);
@@ -527,12 +535,15 @@ inline void measure_segments(GetC getC, GetR getR, GetL getL, std::size_t nrun,
527535
sC = dc;
528536
sR = dr;
529537
if (sC < P || sR < Q) break;
538+
// A segment of off+1 slabs reaches element off*sC+P-1 of C and
539+
// off*sR+Q-1 of R, both of which the BLAS has to be able to index.
540+
// The strides and extents are fixed from here on, so bound off once.
541+
max_off = std::min(math::blas::max_ld_offset(sC, P),
542+
math::blas::max_ld_offset(sR, Q));
530543
} else if (dc != off * sC || dr != off * sR) {
531544
break;
532545
}
533-
// admitting this cell makes the segment off+1 slabs long; the GEMM
534-
// would then reach element off*sC+P-1 of C and off*sR+Q-1 of R.
535-
if (!ld_fits(sC, off + 1, P) || !ld_fits(sR, off + 1, Q)) break;
546+
if (off > max_off) break;
536547
++end;
537548
}
538549
const std::size_t len = end - mu;
@@ -1293,8 +1304,8 @@ void arena_strided_gemm_ce_e(ResultOuter& C, const LeftOuter& L,
12931304
ldA = static_cast<long>(lc[lbase + a_off(m, 1)].data() - l0.data());
12941305
ldB = static_cast<long>(rc[rbase + b_off(1, n)].data() - r0.data());
12951306
if (ldA < P || ldB < Q) clean = false;
1296-
if (!ld_fits(ldA, static_cast<long>(K), P) ||
1297-
!ld_fits(ldB, static_cast<long>(K), Q))
1307+
if (!math::blas::ld_fits(ldA, static_cast<long>(K), P) ||
1308+
!math::blas::ld_fits(ldB, static_cast<long>(K), Q))
12981309
clean = false;
12991310
for (std::size_t k = 0; clean && k < K; ++k) {
13001311
if (lc[lbase + a_off(m, k)].data() !=
@@ -1583,6 +1594,7 @@ void arena_strided_gemm_ce_ce_right(ResultOuter& C, const LeftOuter& L,
15831594
// reuse a run-wide stale stride).
15841595
std::size_t end = mu + 1;
15851596
long sR = -1, sC = -1;
1597+
long max_off = 0; // set with the strides, below
15861598
while (end < Mmu) {
15871599
const auto& rce = rc[rbase + r_off(k, end)];
15881600
const auto& cce = cc[cbase + c_off(m, end)];
@@ -1597,13 +1609,16 @@ void arena_strided_gemm_ce_ce_right(ResultOuter& C, const LeftOuter& L,
15971609
sR = dR;
15981610
sC = dC;
15991611
if (sR < Q || sC < P) break; // page-jump / overlap
1612+
// A segment of off+1 slabs reaches element off*sR+Q-1 of A and
1613+
// off*sC+P-1 of C, both of which the BLAS has to be able to
1614+
// index. The strides and extents are fixed from here on, so
1615+
// bound off once rather than dividing per admitted cell.
1616+
max_off = std::min(blas::max_ld_offset(sR, Q),
1617+
blas::max_ld_offset(sC, P));
16001618
} else if (dR != off * sR || dC != off * sC) {
16011619
break;
16021620
}
1603-
// admitting this cell makes the segment off+1 slabs long; the
1604-
// GEMM would then reach element off*sR+Q-1 of A and off*sC+P-1
1605-
// of C.
1606-
if (!ld_fits(sR, off + 1, Q) || !ld_fits(sC, off + 1, P)) break;
1621+
if (off > max_off) break;
16071622
++end;
16081623
}
16091624
const std::size_t Mseg = end - mu;
@@ -1830,6 +1845,7 @@ void arena_strided_gemm_ce_ce_left(ResultOuter& C, const LeftOuter& L,
18301845
// reuse a run-wide stale stride).
18311846
std::size_t end = m + 1;
18321847
long sA = -1, sC = -1;
1848+
long max_off = 0; // set with the strides, below
18331849
while (end < Mo) {
18341850
const auto& lce = lc[lbase + l_off(end, k)];
18351851
const auto& cce = cc[cbase + c_off(end, n)];
@@ -1844,13 +1860,16 @@ void arena_strided_gemm_ce_ce_left(ResultOuter& C, const LeftOuter& L,
18441860
sA = dA;
18451861
sC = dC;
18461862
if (sA < Q || sC < P) break; // page-jump / overlap
1863+
// A segment of off+1 slabs reaches element off*sA+Q-1 of A and
1864+
// off*sC+P-1 of C, both of which the BLAS has to be able to
1865+
// index. The strides and extents are fixed from here on, so
1866+
// bound off once rather than dividing per admitted cell.
1867+
max_off = std::min(blas::max_ld_offset(sA, Q),
1868+
blas::max_ld_offset(sC, P));
18471869
} else if (dA != off * sA || dC != off * sC) {
18481870
break;
18491871
}
1850-
// admitting this cell makes the segment off+1 slabs long; the
1851-
// GEMM would then reach element off*sA+Q-1 of A and off*sC+P-1
1852-
// of C.
1853-
if (!ld_fits(sA, off + 1, Q) || !ld_fits(sC, off + 1, P)) break;
1872+
if (off > max_off) break;
18541873
++end;
18551874
}
18561875
const std::size_t Mseg = end - m;

‎tests/CMakeLists.txt‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,7 @@ set(ta_test_src_files ta_test.cpp
9797
dot_inner.cpp
9898
general_product.cpp
9999
linalg.cpp
100+
math_blas.cpp
100101
cp.cpp
101102
btas.cpp
102103
arena.cpp

‎tests/arena_strided_gemm.cpp‎

Lines changed: 33 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -391,8 +391,17 @@ BOOST_AUTO_TEST_CASE(ce_e_stride_past_blas_int_falls_back) {
391391
// second cell sits in lazily reserved address space, so only the two cells'
392392
// pages are ever touched; if the reservation is refused the case is skipped.
393393
namespace blas = TiledArray::math::blas;
394-
if (blas::max_ld() >= std::numeric_limits<blas::integer>::max()) return;
395394
const std::size_t M = 1, N = 1, K = 2, P = 3, Q = 4;
395+
// Always run, so neither skip below leaves the case without a single
396+
// assertion: an ordinary contiguous 2-cell run is still accepted.
397+
BOOST_CHECK(blas::ld_fits(static_cast<blas::integer>(Q), 2,
398+
static_cast<blas::integer>(Q)));
399+
if (blas::max_ld() >= std::numeric_limits<blas::integer>::max()) {
400+
BOOST_TEST_MESSAGE(
401+
"ILP64: blas_int is as wide as integer, so no stride can outrun it; "
402+
"skipped");
403+
return;
404+
}
396405
const std::size_t stride = static_cast<std::size_t>(blas::max_ld()) + 1;
397406
const std::size_t align = Inner::cell_alignment();
398407
const std::size_t len = stride * sizeof(double) + Inner::cell_size(P) + align;
@@ -403,14 +412,28 @@ BOOST_AUTO_TEST_CASE(ce_e_stride_past_blas_int_falls_back) {
403412
<< " bytes of address space; skipped");
404413
return;
405414
}
406-
const auto unmap = [len](void* p) { ::munmap(p, len); };
407-
std::unique_ptr<void, decltype(unmap)> mapping(mem, unmap);
415+
using Cell = Inner::Cell;
416+
// Owns the reservation and the placement-new'd cells together: a failing
417+
// BOOST_REQUIRE below must not skip ~Cell(), whose TA::Range owns a heap
418+
// buffer -- under this repo's ASan debug build that would bury the real
419+
// failure in leak reports.
420+
struct Reservation {
421+
void* mem;
422+
std::size_t len;
423+
Cell* c0 = nullptr;
424+
Cell* c1 = nullptr;
425+
~Reservation() {
426+
if (c1) c1->~Cell();
427+
if (c0) c0->~Cell();
428+
::munmap(mem, len);
429+
}
430+
} reservation{mem, len};
408431
auto* base = reinterpret_cast<std::byte*>(
409432
(reinterpret_cast<std::uintptr_t>(mem) + align - 1) & ~(align - 1));
410433
// stride * sizeof(double) is a power of two >= align, so cell 1 is aligned
411-
using Cell = Inner::Cell;
412-
Cell* c0 = ::new (base) Cell{TA::Range{P}};
413-
Cell* c1 = ::new (base + stride * sizeof(double)) Cell{TA::Range{P}};
434+
Cell* c0 = reservation.c0 = ::new (base) Cell{TA::Range{P}};
435+
Cell* c1 = reservation.c1 =
436+
::new (base + stride * sizeof(double)) Cell{TA::Range{P}};
414437
{
415438
Inner l0(c0), l1(c1);
416439
BOOST_REQUIRE_EQUAL(static_cast<std::size_t>(l1.data() - l0.data()),
@@ -423,9 +446,9 @@ BOOST_AUTO_TEST_CASE(ce_e_stride_past_blas_int_falls_back) {
423446
Inner* raw = alloc.allocate(K);
424447
::new (raw) Inner(l0);
425448
::new (raw + 1) Inner(l1);
426-
std::shared_ptr<Inner[]> ldata(raw, [alloc, K](Inner* p) mutable {
427-
for (std::size_t i = 0; i < K; ++i) (p + i)->~Inner();
428-
alloc.deallocate(p, K);
449+
std::shared_ptr<Inner[]> ldata(raw, [alloc, n = K](Inner* p) mutable {
450+
for (std::size_t i = 0; i < n; ++i) (p + i)->~Inner();
451+
alloc.deallocate(p, n);
429452
});
430453
Outer L(TA::Range{M, K}, /*nbatch=*/1, std::move(ldata));
431454
Outer R = make_filled(
@@ -438,9 +461,7 @@ BOOST_AUTO_TEST_CASE(ce_e_stride_past_blas_int_falls_back) {
438461
const double* got = C.data()[0].data();
439462
for (std::size_t e = 0; e < P * Q; ++e)
440463
BOOST_CHECK_CLOSE(got[e], ref[e], 1e-12);
441-
} // the views die before their cells
442-
c1->~Cell();
443-
c0->~Cell();
464+
} // the views die before their cells, which ~Reservation then destroys
444465
}
445466
#ifdef TA_STRIDED_GEMM_COUNT
446467
BOOST_AUTO_TEST_CASE(ce_e_fires_clean_path) {

0 commit comments

Comments
 (0)