Skip to content

Commit 766de74

Browse files
authored
Merge branch 'master' into asv-benchmarks
2 parents 70e9e72 + b21a22c commit 766de74

2 files changed

Lines changed: 160 additions & 119 deletions

File tree

‎CHANGELOG.md‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
2323
* Raised the minimum build-time `Cython` requirement to `3.1.0`, the first release providing the `freethreading_compatible` directive [gh-159](https://github.com/IntelPython/mkl_random/pull/159)
2424
* Extended the `memcpy`-based fast path of `shuffle` to multi-dimensional `ndarray` inputs whose first-axis items are contiguous, which is also much faster than the previous buffered path [gh-159](https://github.com/IntelPython/mkl_random/pull/159)
2525
* Updated the `randint_untyped` implementation, which now supports `array_like` (broadcastable) `low`/`high` bounds [gh-176](https://github.com/IntelPython/mkl_random/pull/176)
26+
* Sped up `randint` with `array_like` bounds by generating words in cache-sized chunks and by picking the Lemire rejection test by range width [gh-173](https://github.com/IntelPython/mkl_random/pull/173)
27+
* Reduced the memory use and sped up integer generation with `WH`, `MCG31`, `R250` and `MRG32K3A`, which lack `viRngUniformBits` support [gh-178](https://github.com/IntelPython/mkl_random/pull/178)
2628

2729
### Fixed
2830
* Fixed an out-of-range integer `brng` indexing `brng_list` past its end, which returned uninitialized memory as random values; it now warns and falls back to `MT19937` [gh-177](https://github.com/IntelPython/mkl_random/pull/177)

‎mkl_random/src/mkl_distributions.cpp‎

Lines changed: 158 additions & 119 deletions
Original file line numberDiff line numberDiff line change
@@ -1594,85 +1594,76 @@ void irk_logseries_vec(irk_state *state,
15941594
mkl_free(Uvec);
15951595
}
15961596

1597-
/*
1598-
* Bulk source of raw uniform words for the bounded-integer
1599-
* routines below, overloaded on the word type (32/64-bit). BRNGs that lack
1600-
* viRngUniformBits fall back to assembling words from viRngUniform.
1601-
*/
1602-
static inline void
1603-
irk_uniform_bits_vec(irk_state *state, npy_intp len, npy_uint32 *buf)
1597+
/* viRngUniform is the only bit source for BRNGs without viRngUniformBits, and
1598+
* it emits 32-bit integers, so each word is built from 16-bit halves. */
1599+
template <typename WT>
1600+
static void irk_uniform_bits_fallback(irk_state *state, npy_intp len, WT *buf)
16041601
{
16051602
int err = 0;
1603+
const int words = sizeof(WT) / 2;
1604+
const npy_intp tile_len = 4096;
1605+
/* a cache-resident tile avoids allocating 2 or 4 halves per element */
1606+
int tile[tile_len];
16061607
npy_intp i = 0;
1608+
int h = 0;
16071609

16081610
while (len > 0) {
1609-
MKL_INT c = (len > MKL_INT_MAX) ? (MKL_INT)MKL_INT_MAX : (MKL_INT)len;
1610-
err = viRngUniformBits32(VSL_RNG_METHOD_UNIFORMBITS32_STD,
1611-
state->stream, c, (unsigned int *)buf);
1612-
if (err == VSL_RNG_ERROR_BRNG_NOT_SUPPORTED) {
1613-
/* viRngUniformBits32 unsupported for WH/MCG31/R250/MRG32K3A;
1614-
* build each word from two 16-bit viRngUniform halves */
1615-
npy_intp total = 2 * (npy_intp)c, rem = total, off = 0;
1616-
int *tmp = (int *)mkl_malloc(total * sizeof(int), 64);
1617-
assert(tmp != nullptr);
1618-
/* one call unless the count exceeds MKL_INT */
1619-
while (rem > 0) {
1620-
MKL_INT cc =
1621-
(rem > MKL_INT_MAX) ? (MKL_INT)MKL_INT_MAX : (MKL_INT)rem;
1622-
err = viRngUniform(VSL_RNG_METHOD_UNIFORM_STD, state->stream,
1623-
cc, tmp + off, 0, 65536);
1624-
assert(err == VSL_STATUS_OK);
1625-
off += cc;
1626-
rem -= cc;
1627-
}
1628-
for (i = 0; i < c; ++i)
1629-
buf[i] = ((npy_uint32)tmp[2 * i]) |
1630-
(((npy_uint32)tmp[2 * i + 1]) << 16);
1631-
mkl_free(tmp);
1632-
}
1633-
else {
1634-
assert(err == VSL_STATUS_OK);
1611+
const npy_intp n = (len < tile_len / words) ? len : tile_len / words;
1612+
err = viRngUniform(VSL_RNG_METHOD_UNIFORM_STD, state->stream,
1613+
(int)(n * words), tile, 0, 65536);
1614+
assert(err == VSL_STATUS_OK);
1615+
1616+
for (i = 0; i < n; ++i) {
1617+
WT w = 0;
1618+
1619+
for (h = 0; h < words; ++h)
1620+
w |= ((WT)(npy_uint32)tile[i * words + h]) << (16 * h);
1621+
1622+
buf[i] = w;
16351623
}
1636-
buf += c;
1637-
len -= c;
1624+
1625+
buf += n;
1626+
len -= n;
16381627
}
16391628
}
16401629

1641-
static inline void
1642-
irk_uniform_bits_vec(irk_state *state, npy_intp len, npy_uint64 *buf)
1630+
static inline int
1631+
irk_uniform_bits_call(irk_state *state, MKL_INT count, npy_uint32 *buf)
1632+
{
1633+
return viRngUniformBits32(VSL_RNG_METHOD_UNIFORMBITS32_STD, state->stream,
1634+
count, (unsigned int *)buf);
1635+
}
1636+
1637+
static inline int
1638+
irk_uniform_bits_call(irk_state *state, MKL_INT count, npy_uint64 *buf)
1639+
{
1640+
return viRngUniformBits64(VSL_RNG_METHOD_UNIFORMBITS64_STD, state->stream,
1641+
count, (unsigned MKL_INT64 *)buf);
1642+
}
1643+
1644+
/*
1645+
* Bulk source of raw uniform words for the bounded-integer
1646+
* routines below, templated on the word type (32/64-bit). BRNGs that lack
1647+
* viRngUniformBits fall back to assembling words from viRngUniform.
1648+
*/
1649+
template <typename WT>
1650+
static inline void irk_uniform_bits_vec(irk_state *state, npy_intp len, WT *buf)
16431651
{
16441652
int err = 0;
1645-
npy_intp i = 0;
16461653
/* viRngUniformBits64 counts 32-bit words, so its count must fit half of
16471654
* MKL_INT; larger requests under-fill the buffer or crash */
1648-
const npy_intp bits64_max = MKL_INT_MAX / 2;
1655+
const npy_intp max_count =
1656+
(sizeof(WT) == 8) ? MKL_INT_MAX / 2 : MKL_INT_MAX;
1657+
1658+
static_assert(sizeof(WT) == 4 || sizeof(WT) == 8,
1659+
"uniform bits are generated in 32- or 64-bit words");
16491660

16501661
while (len > 0) {
1651-
MKL_INT c = (len > bits64_max) ? (MKL_INT)bits64_max : (MKL_INT)len;
1652-
err = viRngUniformBits64(VSL_RNG_METHOD_UNIFORMBITS64_STD,
1653-
state->stream, c, (unsigned MKL_INT64 *)buf);
1662+
MKL_INT c = (len > max_count) ? (MKL_INT)max_count : (MKL_INT)len;
1663+
err = irk_uniform_bits_call(state, c, buf);
16541664
if (err == VSL_RNG_ERROR_BRNG_NOT_SUPPORTED) {
1655-
/* viRngUniformBits64 unsupported for WH/MCG31/R250/MRG32K3A;
1656-
* build each word from four 16-bit viRngUniform halves */
1657-
npy_intp total = 4 * (npy_intp)c, rem = total, off = 0;
1658-
int *tmp = (int *)mkl_malloc(total * sizeof(int), 64);
1659-
assert(tmp != nullptr);
1660-
/* one call unless the count exceeds MKL_INT */
1661-
while (rem > 0) {
1662-
MKL_INT cc =
1663-
(rem > MKL_INT_MAX) ? (MKL_INT)MKL_INT_MAX : (MKL_INT)rem;
1664-
err = viRngUniform(VSL_RNG_METHOD_UNIFORM_STD, state->stream,
1665-
cc, tmp + off, 0, 65536);
1666-
assert(err == VSL_STATUS_OK);
1667-
off += cc;
1668-
rem -= cc;
1669-
}
1670-
for (i = 0; i < c; ++i)
1671-
buf[i] = ((npy_uint64)(npy_uint32)tmp[4 * i]) |
1672-
(((npy_uint64)(npy_uint32)tmp[4 * i + 1]) << 16) |
1673-
(((npy_uint64)(npy_uint32)tmp[4 * i + 2]) << 32) |
1674-
(((npy_uint64)(npy_uint32)tmp[4 * i + 3]) << 48);
1675-
mkl_free(tmp);
1665+
/* unsupported for WH/MCG31/R250/MRG32K3A */
1666+
irk_uniform_bits_fallback(state, (npy_intp)c, buf);
16761667
}
16771668
else {
16781669
assert(err == VSL_STATUS_OK);
@@ -2054,8 +2045,9 @@ static inline npy_uint64 irk_mulhi(npy_uint64 a, npy_uint64 b, npy_uint64 *lo)
20542045
/*
20552046
* Draw res[i] uniformly from [low[i], hi[i]] (inclusive) using Lemire's
20562047
* multiply-shift method (per-element bounds, same as NumPy).
2057-
* Words are generated in bulk by MKL; the rare rejected elements are
2058-
* gathered into `idx` (allocated lazily) and retried on the next round.
2048+
* Words are generated by MKL in cache-sized chunks; the rejected
2049+
* elements are gathered into `idx` (allocated lazily) and retried
2050+
* locally within each chunk.
20592051
* T is the result type, UT its unsigned counterpart,
20602052
* WT the raw-word type (s wraps to 0 for a full-range draw).
20612053
*/
@@ -2066,76 +2058,123 @@ static void irk_rand_bounded_broadcast(irk_state *state,
20662058
const T *low,
20672059
const T *hi)
20682060
{
2069-
npy_intp i = 0;
2070-
npy_intp k = 0;
2071-
npy_intp n_pending = 0;
2072-
npy_intp *idx = nullptr;
2073-
WT *words = nullptr;
2061+
npy_intp *idx = nullptr; /* reject indices */
20742062

20752063
if (len < 1)
20762064
return;
20772065

2078-
/* TODO: possible speedup :
2079-
* generate and consume words in cache-sized chunks
2080-
* instead of one full-length pass */
2081-
words = (WT *)mkl_malloc(len * sizeof(WT), 64);
2082-
assert(words != nullptr);
2066+
/* Optimized path:
2067+
* cache-sized chunks instead of one full-length pass */
2068+
const npy_intp CHUNK_SIZE = 1 << 15; /* ~32K elements per chunk */
2069+
npy_intp chunk_cap = (len < CHUNK_SIZE) ? len : CHUNK_SIZE;
20832070

2084-
irk_uniform_bits_vec(state, len, words);
2071+
WT *words = (WT *)mkl_malloc(chunk_cap * sizeof(WT), 64);
2072+
assert(words != nullptr);
20852073

2086-
for (i = 0; i < len; ++i) {
2087-
WT w = (WT)words[i];
2088-
/* diff cast back to UT so narrow types wrap (no signed promotion) */
2089-
UT d = (UT)(((UT)hi[i]) - ((UT)low[i]));
2090-
WT s = (WT)d + 1; /* 0 iff full range (32/64-bit only) */
2091-
WT result = w;
2092-
2093-
if (s != 0) {
2094-
WT lo = 0;
2095-
result = irk_mulhi(w, s, &lo);
2096-
if (lo < s) { /* rare */
2097-
WT t = (WT)(0 - s) % s;
2098-
if (lo < t) {
2099-
if (idx == nullptr) {
2100-
idx =
2101-
(npy_intp *)mkl_malloc(len * sizeof(npy_intp), 64);
2102-
assert(idx != nullptr);
2074+
/* `lo < s` is free for narrow ranges, but mispredicts for wide ones:
2075+
* count it on the first chunk, then pick the cheaper test */
2076+
npy_intp n_hits = 0;
2077+
bool wide = false;
2078+
/* memoized reject threshold */
2079+
WT last_s = 0, last_t = 0;
2080+
2081+
for (npy_intp base = 0; base < len; base += chunk_cap) {
2082+
npy_intp chunk = (len - base < chunk_cap) ? (len - base) : chunk_cap;
2083+
npy_intp n_pending = 0;
2084+
2085+
irk_uniform_bits_vec(state, chunk, words);
2086+
2087+
if (wide) {
2088+
for (npy_intp i = 0; i < chunk; ++i) {
2089+
npy_intp j = base + i;
2090+
WT w = (WT)words[i];
2091+
UT d = (UT)(((UT)hi[j]) - ((UT)low[j]));
2092+
WT s = (WT)d + 1;
2093+
WT result = w;
2094+
2095+
if (s != 0) {
2096+
WT lo = 0;
2097+
result = irk_mulhi(w, s, &lo);
2098+
if (s != last_s) {
2099+
last_t = (WT)(0 - s) % s;
2100+
last_s = s;
2101+
}
2102+
if (lo < last_t) {
2103+
if (idx == nullptr) {
2104+
idx = (npy_intp *)mkl_malloc(
2105+
chunk_cap * sizeof(npy_intp), 64);
2106+
assert(idx != nullptr);
2107+
}
2108+
idx[n_pending++] = j;
2109+
continue;
21032110
}
2104-
idx[n_pending++] = i;
2105-
continue;
21062111
}
2112+
res[j] = (T)(((UT)low[j]) + (UT)result);
21072113
}
21082114
}
2109-
res[i] = (T)(((UT)low[i]) + (UT)result);
2110-
}
2111-
2112-
while (n_pending > 0) {
2113-
npy_intp wpos = 0;
2114-
2115-
irk_uniform_bits_vec(state, n_pending, words);
2115+
else {
2116+
for (npy_intp i = 0; i < chunk; ++i) {
2117+
npy_intp j = base + i;
2118+
WT w = (WT)words[i];
2119+
/* diff cast back to UT so narrow types wrap (no signed
2120+
* promotion) */
2121+
UT d = (UT)(((UT)hi[j]) - ((UT)low[j]));
2122+
WT s = (WT)d + 1; /* 0 iff full range (32/64-bit only) */
2123+
WT result = w;
2124+
2125+
if (s != 0) {
2126+
WT lo = 0;
2127+
result = irk_mulhi(w, s, &lo);
2128+
if (lo < s) { /* rare */
2129+
WT t = (WT)(0 - s) % s;
2130+
++n_hits;
2131+
if (lo < t) {
2132+
if (idx == nullptr) {
2133+
idx = (npy_intp *)mkl_malloc(
2134+
chunk_cap * sizeof(npy_intp), 64);
2135+
assert(idx != nullptr);
2136+
}
2137+
idx[n_pending++] = j;
2138+
continue;
2139+
}
2140+
}
2141+
}
2142+
res[j] = (T)(((UT)low[j]) + (UT)result);
2143+
}
21162144

2117-
for (k = 0; k < n_pending; ++k) {
2118-
npy_intp j = idx[k];
2119-
WT w = (WT)words[k];
2120-
UT d = (UT)(((UT)hi[j]) - ((UT)low[j]));
2121-
WT s = (WT)d + 1;
2122-
WT result = w;
2145+
if (base == 0)
2146+
wide = n_hits > chunk / 16;
2147+
}
21232148

2124-
if (s != 0) {
2125-
WT lo = 0;
2126-
result = irk_mulhi(w, s, &lo);
2127-
if (lo < s) {
2128-
WT t = (WT)(0 - s) % s;
2129-
if (lo < t) {
2130-
/* keep pending; wpos <= k so idx[k] read first */
2131-
idx[wpos++] = j;
2132-
continue;
2149+
/* retry the chunk's rejects locally with fresh words */
2150+
while (n_pending > 0) {
2151+
npy_intp wpos = 0;
2152+
2153+
irk_uniform_bits_vec(state, n_pending, words);
2154+
2155+
for (npy_intp k = 0; k < n_pending; ++k) {
2156+
npy_intp j = idx[k];
2157+
WT w = (WT)words[k];
2158+
UT d = (UT)(((UT)hi[j]) - ((UT)low[j]));
2159+
WT s = (WT)d + 1;
2160+
WT result = w;
2161+
2162+
if (s != 0) {
2163+
WT lo = 0;
2164+
result = irk_mulhi(w, s, &lo);
2165+
if (lo < s) {
2166+
WT t = (WT)(0 - s) % s;
2167+
if (lo < t) {
2168+
/* keep pending; wpos <= k so idx[k] read first */
2169+
idx[wpos++] = j;
2170+
continue;
2171+
}
21332172
}
21342173
}
2174+
res[j] = (T)(((UT)low[j]) + (UT)result);
21352175
}
2136-
res[j] = (T)(((UT)low[j]) + (UT)result);
2176+
n_pending = wpos;
21372177
}
2138-
n_pending = wpos;
21392178
}
21402179

21412180
if (idx != nullptr)

0 commit comments

Comments
 (0)