rns-engine 0.6.0__tar.gz → 0.7.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {rns_engine-0.6.0/src/rns_engine.egg-info → rns_engine-0.7.0}/PKG-INFO +7 -5
- {rns_engine-0.6.0 → rns_engine-0.7.0}/README.md +6 -4
- {rns_engine-0.6.0 → rns_engine-0.7.0}/pyproject.toml +1 -1
- {rns_engine-0.6.0 → rns_engine-0.7.0}/setup.py +16 -8
- rns_engine-0.7.0/src/rns_engine/_weighted.cpp +141 -0
- {rns_engine-0.6.0 → rns_engine-0.7.0}/src/rns_engine/weighted.py +47 -38
- {rns_engine-0.6.0 → rns_engine-0.7.0/src/rns_engine.egg-info}/PKG-INFO +7 -5
- {rns_engine-0.6.0 → rns_engine-0.7.0}/src/rns_engine.egg-info/SOURCES.txt +3 -1
- {rns_engine-0.6.0 → rns_engine-0.7.0}/tests/test_weighted.py +50 -0
- rns_engine-0.7.0/tests/test_weighted_benchmark.py +71 -0
- {rns_engine-0.6.0 → rns_engine-0.7.0}/LICENSE +0 -0
- {rns_engine-0.6.0 → rns_engine-0.7.0}/setup.cfg +0 -0
- {rns_engine-0.6.0 → rns_engine-0.7.0}/src/rns_engine/__init__.py +0 -0
- {rns_engine-0.6.0 → rns_engine-0.7.0}/src/rns_engine/_core.cpp +0 -0
- {rns_engine-0.6.0 → rns_engine-0.7.0}/src/rns_engine/engine.py +0 -0
- {rns_engine-0.6.0 → rns_engine-0.7.0}/src/rns_engine/signed.py +0 -0
- {rns_engine-0.6.0 → rns_engine-0.7.0}/src/rns_engine.egg-info/dependency_links.txt +0 -0
- {rns_engine-0.6.0 → rns_engine-0.7.0}/src/rns_engine.egg-info/not-zip-safe +0 -0
- {rns_engine-0.6.0 → rns_engine-0.7.0}/src/rns_engine.egg-info/requires.txt +0 -0
- {rns_engine-0.6.0 → rns_engine-0.7.0}/src/rns_engine.egg-info/top_level.txt +0 -0
- {rns_engine-0.6.0 → rns_engine-0.7.0}/tests/test_packaging.py +0 -0
- {rns_engine-0.6.0 → rns_engine-0.7.0}/tests/test_rns.py +0 -0
- {rns_engine-0.6.0 → rns_engine-0.7.0}/tests/test_session.py +0 -0
- {rns_engine-0.6.0 → rns_engine-0.7.0}/tests/test_signed.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: rns_engine
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.7.0
|
|
4
4
|
Summary: Exact RNS engine with session/cache API.
|
|
5
5
|
Author: Evan Wesley
|
|
6
6
|
License-Expression: AGPL-3.0-only
|
|
@@ -194,7 +194,7 @@ out = session.decode_signed(session.add(a, b))
|
|
|
194
194
|
|
|
195
195
|
## Weighted INT32 partial accumulation
|
|
196
196
|
|
|
197
|
-
Version 0.
|
|
197
|
+
Version 0.7 provides the fused direct bridge for digit-plane, chunked-GEMM, and Tensor Core pipelines.
|
|
198
198
|
|
|
199
199
|
Give the engine signed INT32 partial outputs with shape:
|
|
200
200
|
|
|
@@ -246,7 +246,7 @@ Important details:
|
|
|
246
246
|
- `decode_modular()` always returns the canonical residue in `[0, M)`.
|
|
247
247
|
- Empty term sets produce an exact zero result.
|
|
248
248
|
|
|
249
|
-
The current implementation
|
|
249
|
+
The current implementation uses one fused native `_weighted` call to read signed INT32 partials, collect exact per-term magnitude bounds, apply positional weights modulo `M`, and accumulate all four rails. The pre-fusion staged encode → scale → add body remains available internally as an exact reference for A/B tests. Local AVX2 diagnostics measured roughly 2.1×–2.9× speedup at one million outputs and larger gains on tiny many-term cases; those figures are hardware-specific CPU evidence, not CUDA, Tensor Core, or universal performance claims.
|
|
250
250
|
|
|
251
251
|
A precomputed receipt can also be created without materializing partial arrays:
|
|
252
252
|
|
|
@@ -404,9 +404,11 @@ rns.HAS_AVX2
|
|
|
404
404
|
|
|
405
405
|
## Current release
|
|
406
406
|
|
|
407
|
-
### v0.
|
|
407
|
+
### v0.7.0
|
|
408
408
|
|
|
409
|
-
- weighted signed INT32 partial accumulation
|
|
409
|
+
- fused native weighted signed INT32 partial accumulation
|
|
410
|
+
- one compiled pass for magnitude collection, positional weighting, and four-rail accumulation
|
|
411
|
+
- staged reference body retained for exact fused-versus-staged A/B benchmarking
|
|
410
412
|
- exact arbitrary-precision positional weights
|
|
411
413
|
- per-term magnitude receipts and strict no-wrap certification
|
|
412
414
|
- canonical modular and uniqueness-gated signed decode paths
|
|
@@ -171,7 +171,7 @@ out = session.decode_signed(session.add(a, b))
|
|
|
171
171
|
|
|
172
172
|
## Weighted INT32 partial accumulation
|
|
173
173
|
|
|
174
|
-
Version 0.
|
|
174
|
+
Version 0.7 provides the fused direct bridge for digit-plane, chunked-GEMM, and Tensor Core pipelines.
|
|
175
175
|
|
|
176
176
|
Give the engine signed INT32 partial outputs with shape:
|
|
177
177
|
|
|
@@ -223,7 +223,7 @@ Important details:
|
|
|
223
223
|
- `decode_modular()` always returns the canonical residue in `[0, M)`.
|
|
224
224
|
- Empty term sets produce an exact zero result.
|
|
225
225
|
|
|
226
|
-
The current implementation
|
|
226
|
+
The current implementation uses one fused native `_weighted` call to read signed INT32 partials, collect exact per-term magnitude bounds, apply positional weights modulo `M`, and accumulate all four rails. The pre-fusion staged encode → scale → add body remains available internally as an exact reference for A/B tests. Local AVX2 diagnostics measured roughly 2.1×–2.9× speedup at one million outputs and larger gains on tiny many-term cases; those figures are hardware-specific CPU evidence, not CUDA, Tensor Core, or universal performance claims.
|
|
227
227
|
|
|
228
228
|
A precomputed receipt can also be created without materializing partial arrays:
|
|
229
229
|
|
|
@@ -381,9 +381,11 @@ rns.HAS_AVX2
|
|
|
381
381
|
|
|
382
382
|
## Current release
|
|
383
383
|
|
|
384
|
-
### v0.
|
|
384
|
+
### v0.7.0
|
|
385
385
|
|
|
386
|
-
- weighted signed INT32 partial accumulation
|
|
386
|
+
- fused native weighted signed INT32 partial accumulation
|
|
387
|
+
- one compiled pass for magnitude collection, positional weighting, and four-rail accumulation
|
|
388
|
+
- staged reference body retained for exact fused-versus-staged A/B benchmarking
|
|
387
389
|
- exact arbitrary-precision positional weights
|
|
388
390
|
- per-term magnitude receipts and strict no-wrap certification
|
|
389
391
|
- canonical modular and uniqueness-gated signed decode paths
|
|
@@ -39,19 +39,27 @@ def get_link_args():
|
|
|
39
39
|
return []
|
|
40
40
|
|
|
41
41
|
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
42
|
+
def native_extension(name, source):
|
|
43
|
+
return Extension(
|
|
44
|
+
name,
|
|
45
|
+
sources=[source],
|
|
46
|
+
include_dirs=[pybind11.get_include(), np.get_include()],
|
|
47
|
+
extra_compile_args=get_compile_args(),
|
|
48
|
+
extra_link_args=get_link_args(),
|
|
49
|
+
language="c++",
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
ext = native_extension("rns_engine._core", "src/rns_engine/_core.cpp")
|
|
54
|
+
weighted_ext = native_extension(
|
|
55
|
+
"rns_engine._weighted",
|
|
56
|
+
"src/rns_engine/_weighted.cpp",
|
|
49
57
|
)
|
|
50
58
|
|
|
51
59
|
# Project metadata and package discovery live in pyproject.toml. Keeping them
|
|
52
60
|
# out of setup.py prevents the two build entry points from drifting apart.
|
|
53
61
|
setup(
|
|
54
|
-
ext_modules=[ext],
|
|
62
|
+
ext_modules=[ext, weighted_ext],
|
|
55
63
|
include_package_data=True,
|
|
56
64
|
zip_safe=False,
|
|
57
65
|
)
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
#include <pybind11/pybind11.h>
|
|
2
|
+
#include <pybind11/numpy.h>
|
|
3
|
+
#include <stdint.h>
|
|
4
|
+
#include <stdexcept>
|
|
5
|
+
#include <string>
|
|
6
|
+
#include <cstring>
|
|
7
|
+
|
|
8
|
+
namespace py = pybind11;
|
|
9
|
+
|
|
10
|
+
using arr_i32 = py::array_t<int32_t, py::array::c_style>;
|
|
11
|
+
using arr16 = py::array_t<uint16_t, py::array::c_style>;
|
|
12
|
+
using arr32 = py::array_t<uint32_t, py::array::c_style>;
|
|
13
|
+
using arr64 = py::array_t<uint64_t, py::array::c_style | py::array::forcecast>;
|
|
14
|
+
|
|
15
|
+
static constexpr uint32_t M0 = 127;
|
|
16
|
+
static constexpr uint32_t M1 = 8191;
|
|
17
|
+
static constexpr uint32_t M2 = 65536;
|
|
18
|
+
static constexpr uint32_t M3 = 524287;
|
|
19
|
+
static constexpr uint32_t M3_MASK = 0x7FFFFu;
|
|
20
|
+
static constexpr uint64_t BM = (uint64_t)M0 * M1 * M2 * M3;
|
|
21
|
+
|
|
22
|
+
static int64_t require_1d_len(const py::array& a, const char* name) {
|
|
23
|
+
if (a.ndim() != 1) {
|
|
24
|
+
throw std::invalid_argument(std::string(name) + " must be a 1D array");
|
|
25
|
+
}
|
|
26
|
+
return (int64_t)a.shape(0);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
static inline uint16_t r127s(uint32_t x) {
|
|
30
|
+
x = (x & 0x7F) + (x >> 7);
|
|
31
|
+
x = (x & 0x7F) + (x >> 7);
|
|
32
|
+
return x >= M0 ? x - M0 : (uint16_t)x;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
static inline uint16_t r8191s(uint32_t x) {
|
|
36
|
+
x = (x & 0x1FFF) + (x >> 13);
|
|
37
|
+
x = (x & 0x1FFF) + (x >> 13);
|
|
38
|
+
return x >= M1 ? x - M1 : (uint16_t)x;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
static inline uint32_t fma524287s(uint32_t a, uint32_t b, uint32_t c) {
|
|
42
|
+
uint64_t x = (uint64_t)a * b + c;
|
|
43
|
+
x = (x & M3_MASK) + (x >> 19);
|
|
44
|
+
x = (x & M3_MASK) + (x >> 19);
|
|
45
|
+
x = (x & M3_MASK) + (x >> 19);
|
|
46
|
+
uint32_t r = (uint32_t)x;
|
|
47
|
+
return r >= M3 ? (r - M3) : r;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
static inline uint32_t mod524287_u64(uint64_t x) {
|
|
51
|
+
x = (x & M3_MASK) + (x >> 19);
|
|
52
|
+
x = (x & M3_MASK) + (x >> 19);
|
|
53
|
+
x = (x & M3_MASK) + (x >> 19);
|
|
54
|
+
uint32_t r = (uint32_t)x;
|
|
55
|
+
return r >= M3 ? (r - M3) : r;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
py::tuple weighted_int32(const arr_i32& partials, const arr64& weights) {
|
|
59
|
+
if (partials.ndim() != 2) {
|
|
60
|
+
throw std::invalid_argument("partials must be a 2D int32 array");
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const int64_t terms = (int64_t)partials.shape(0);
|
|
64
|
+
const int64_t n = (int64_t)partials.shape(1);
|
|
65
|
+
const int64_t weight_count = require_1d_len(weights, "weights");
|
|
66
|
+
if (weight_count != terms) {
|
|
67
|
+
throw std::invalid_argument(
|
|
68
|
+
"weights length must match partial term count (" +
|
|
69
|
+
std::to_string(weight_count) + " != " + std::to_string(terms) + ")");
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
arr16 o0({n}), o1({n}), o2({n});
|
|
73
|
+
arr32 o3({n});
|
|
74
|
+
arr64 bounds({terms});
|
|
75
|
+
|
|
76
|
+
uint16_t* p0 = o0.mutable_data();
|
|
77
|
+
uint16_t* p1 = o1.mutable_data();
|
|
78
|
+
uint16_t* p2 = o2.mutable_data();
|
|
79
|
+
uint32_t* p3 = o3.mutable_data();
|
|
80
|
+
uint64_t* pb = bounds.mutable_data();
|
|
81
|
+
|
|
82
|
+
if (n > 0) {
|
|
83
|
+
std::memset(p0, 0, (size_t)n * sizeof(uint16_t));
|
|
84
|
+
std::memset(p1, 0, (size_t)n * sizeof(uint16_t));
|
|
85
|
+
std::memset(p2, 0, (size_t)n * sizeof(uint16_t));
|
|
86
|
+
std::memset(p3, 0, (size_t)n * sizeof(uint32_t));
|
|
87
|
+
}
|
|
88
|
+
if (terms > 0) {
|
|
89
|
+
std::memset(pb, 0, (size_t)terms * sizeof(uint64_t));
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
const int32_t* x = partials.data();
|
|
93
|
+
const uint64_t* w = weights.data();
|
|
94
|
+
|
|
95
|
+
{
|
|
96
|
+
py::gil_scoped_release release;
|
|
97
|
+
for (int64_t term = 0; term < terms; ++term) {
|
|
98
|
+
const uint64_t weight = w[term] % BM;
|
|
99
|
+
const uint16_t w0 = (uint16_t)(weight % M0);
|
|
100
|
+
const uint16_t w1 = (uint16_t)(weight % M1);
|
|
101
|
+
const uint16_t w2 = (uint16_t)weight;
|
|
102
|
+
const uint32_t w3 = mod524287_u64(weight);
|
|
103
|
+
const int32_t* row = x + term * n;
|
|
104
|
+
uint64_t max_abs = 0;
|
|
105
|
+
|
|
106
|
+
for (int64_t i = 0; i < n; ++i) {
|
|
107
|
+
const int32_t signed_value = row[i];
|
|
108
|
+
const uint64_t magnitude = signed_value < 0
|
|
109
|
+
? (uint64_t)(-(int64_t)signed_value)
|
|
110
|
+
: (uint64_t)signed_value;
|
|
111
|
+
if (magnitude > max_abs) max_abs = magnitude;
|
|
112
|
+
|
|
113
|
+
if (weight == 0) continue;
|
|
114
|
+
|
|
115
|
+
const uint64_t canonical = signed_value < 0
|
|
116
|
+
? (uint64_t)((int64_t)BM + (int64_t)signed_value)
|
|
117
|
+
: (uint64_t)signed_value;
|
|
118
|
+
const uint16_t x0 = (uint16_t)(canonical % M0);
|
|
119
|
+
const uint16_t x1 = (uint16_t)(canonical % M1);
|
|
120
|
+
const uint16_t x2 = (uint16_t)canonical;
|
|
121
|
+
const uint32_t x3 = mod524287_u64(canonical);
|
|
122
|
+
|
|
123
|
+
p0[i] = r127s((uint32_t)p0[i] + (uint32_t)x0 * w0);
|
|
124
|
+
p1[i] = r8191s((uint32_t)p1[i] + (uint32_t)x1 * w1);
|
|
125
|
+
p2[i] = (uint16_t)((uint32_t)p2[i] + (uint32_t)x2 * w2);
|
|
126
|
+
p3[i] = fma524287s(x3, w3, p3[i]);
|
|
127
|
+
}
|
|
128
|
+
pb[term] = max_abs;
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
return py::make_tuple(o0, o1, o2, o3, bounds);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
PYBIND11_MODULE(_weighted, m) {
|
|
136
|
+
m.doc() = "Fused weighted signed INT32 accumulation for rns_engine.";
|
|
137
|
+
m.def(
|
|
138
|
+
"weighted_int32",
|
|
139
|
+
&weighted_int32,
|
|
140
|
+
"Fused signed int32 weighting and four-rail accumulation with per-term bounds.");
|
|
141
|
+
}
|
|
@@ -10,6 +10,7 @@ from ._core import M
|
|
|
10
10
|
from ._core import add as _add
|
|
11
11
|
from ._core import decode as _decode
|
|
12
12
|
from ._core import mul_u64 as _mul_u64
|
|
13
|
+
from ._weighted import weighted_int32 as _weighted_int32
|
|
13
14
|
from .engine import EncodedArray
|
|
14
15
|
from .signed import SignedRangeCertificate, certify_signed_bound, decode_signed, encode_signed
|
|
15
16
|
|
|
@@ -57,15 +58,7 @@ def certify_weighted_sum_bound(
|
|
|
57
58
|
term_abs_bounds: Sequence[int] | Iterable[int],
|
|
58
59
|
addend_abs_bound: int = 0,
|
|
59
60
|
) -> SignedRangeCertificate:
|
|
60
|
-
"""Certify ``sum(weight[t] * term[t]) + addend`` from magnitude bounds.
|
|
61
|
-
|
|
62
|
-
The conservative exact bound is::
|
|
63
|
-
|
|
64
|
-
sum(abs(weight[t]) * term_abs_bound[t]) + addend_abs_bound
|
|
65
|
-
|
|
66
|
-
Weights are unrestricted Python integers. The certificate is unique only
|
|
67
|
-
when the resulting bound is strictly less than ``M/2``.
|
|
68
|
-
"""
|
|
61
|
+
"""Certify ``sum(weight[t] * term[t]) + addend`` from magnitude bounds."""
|
|
69
62
|
|
|
70
63
|
exact_weights = _integer_tuple(weights, name="weights")
|
|
71
64
|
exact_bounds = _bound_tuple(term_abs_bounds, name="term_abs_bounds")
|
|
@@ -105,38 +98,18 @@ class WeightedInt32Result:
|
|
|
105
98
|
return int(np.prod(self.output_shape, dtype=np.int64)) if self.output_shape else 1
|
|
106
99
|
|
|
107
100
|
def decode_modular(self) -> np.ndarray:
|
|
108
|
-
"""Return the canonical unsigned residue in ``[0, M)``."""
|
|
109
|
-
|
|
110
101
|
return _decode(*self.encoded.rails()).reshape(self.output_shape)
|
|
111
102
|
|
|
112
103
|
def decode_signed(self, *, require_unique: bool = True) -> np.ndarray:
|
|
113
|
-
"""Return the centered result, optionally requiring uniqueness proof."""
|
|
114
|
-
|
|
115
104
|
if require_unique:
|
|
116
105
|
self.certificate.require_unique()
|
|
117
106
|
return decode_signed(*self.encoded.rails()).reshape(self.output_shape)
|
|
118
107
|
|
|
119
108
|
|
|
120
|
-
def
|
|
109
|
+
def _validate_weighted_inputs(
|
|
121
110
|
partials: Any,
|
|
122
111
|
weights: Sequence[int] | Iterable[int],
|
|
123
|
-
|
|
124
|
-
require_unique: bool = False,
|
|
125
|
-
) -> WeightedInt32Result:
|
|
126
|
-
"""Combine signed INT32 partial outputs through native RNS kernels.
|
|
127
|
-
|
|
128
|
-
``partials`` has shape ``(terms, *output_shape)``. Each term is encoded by
|
|
129
|
-
the native four-rail encoder, multiplied by its exact positional weight
|
|
130
|
-
modulo ``M`` through the native scalar-broadcast kernel, and accumulated by
|
|
131
|
-
the native rail-add kernel. Python orchestrates the terms; the arithmetic on
|
|
132
|
-
every output element remains in the compiled RNS body.
|
|
133
|
-
|
|
134
|
-
The returned range receipt uses the actual maximum absolute INT32 value in
|
|
135
|
-
every term and the full Python integer weights, not their reduced residues.
|
|
136
|
-
Therefore ``certificate.require_unique()`` is a valid no-wrap gate for the
|
|
137
|
-
represented weighted sum.
|
|
138
|
-
"""
|
|
139
|
-
|
|
112
|
+
) -> tuple[np.ndarray, tuple[int, ...], tuple[int, ...], int, np.ndarray]:
|
|
140
113
|
raw = np.asarray(partials)
|
|
141
114
|
if raw.ndim < 1:
|
|
142
115
|
raise ValueError("partials must have shape (terms, *output_shape)")
|
|
@@ -155,6 +128,20 @@ def accumulate_weighted_int32(
|
|
|
155
128
|
output_shape = tuple(int(dimension) for dimension in contiguous.shape[1:])
|
|
156
129
|
output_size = int(np.prod(output_shape, dtype=np.int64)) if output_shape else 1
|
|
157
130
|
flat = contiguous.reshape(term_count, output_size)
|
|
131
|
+
execution_weights = np.fromiter(
|
|
132
|
+
(weight % int(M) for weight in exact_weights),
|
|
133
|
+
dtype=np.uint64,
|
|
134
|
+
count=term_count,
|
|
135
|
+
)
|
|
136
|
+
return flat, exact_weights, output_shape, output_size, execution_weights
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
def _accumulate_weighted_int32_staged(
|
|
140
|
+
flat: np.ndarray,
|
|
141
|
+
exact_weights: tuple[int, ...],
|
|
142
|
+
output_size: int,
|
|
143
|
+
) -> tuple[tuple[np.ndarray, np.ndarray, np.ndarray, np.ndarray], tuple[int, ...]]:
|
|
144
|
+
"""Pre-fusion reference body retained for correctness and A/B benchmarks."""
|
|
158
145
|
|
|
159
146
|
term_abs_bounds: list[int] = []
|
|
160
147
|
for term in flat:
|
|
@@ -164,14 +151,8 @@ def accumulate_weighted_int32(
|
|
|
164
151
|
widened = term.astype(np.int64, copy=False)
|
|
165
152
|
term_abs_bounds.append(int(np.max(np.abs(widened))))
|
|
166
153
|
|
|
167
|
-
bounds = tuple(term_abs_bounds)
|
|
168
|
-
certificate = certify_weighted_sum_bound(exact_weights, bounds)
|
|
169
|
-
if require_unique:
|
|
170
|
-
certificate.require_unique()
|
|
171
|
-
|
|
172
154
|
accumulator = encode_signed(np.zeros(output_size, dtype=np.int64))
|
|
173
155
|
modulus = int(M)
|
|
174
|
-
|
|
175
156
|
for term, weight in zip(flat, exact_weights):
|
|
176
157
|
if weight == 0 or output_size == 0:
|
|
177
158
|
continue
|
|
@@ -179,8 +160,36 @@ def accumulate_weighted_int32(
|
|
|
179
160
|
scaled = _mul_u64(*encoded, weight % modulus)
|
|
180
161
|
accumulator = _add(*accumulator, *scaled)
|
|
181
162
|
|
|
163
|
+
return accumulator, tuple(term_abs_bounds)
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
def accumulate_weighted_int32(
|
|
167
|
+
partials: Any,
|
|
168
|
+
weights: Sequence[int] | Iterable[int],
|
|
169
|
+
*,
|
|
170
|
+
require_unique: bool = False,
|
|
171
|
+
) -> WeightedInt32Result:
|
|
172
|
+
"""Fuse signed INT32 weighting, rail accumulation, and bound collection.
|
|
173
|
+
|
|
174
|
+
Python validates shape and preserves the original arbitrary-precision
|
|
175
|
+
weights for the proof receipt. The compiled kernel reads each INT32 term,
|
|
176
|
+
computes its actual maximum magnitude, reduces the execution weight modulo
|
|
177
|
+
``M``, and updates all four rails without materializing encoded/scaled
|
|
178
|
+
intermediate arrays.
|
|
179
|
+
"""
|
|
180
|
+
|
|
181
|
+
flat, exact_weights, output_shape, _, execution_weights = _validate_weighted_inputs(
|
|
182
|
+
partials,
|
|
183
|
+
weights,
|
|
184
|
+
)
|
|
185
|
+
r0, r1, r2, r3, native_bounds = _weighted_int32(flat, execution_weights)
|
|
186
|
+
bounds = tuple(int(value) for value in native_bounds)
|
|
187
|
+
certificate = certify_weighted_sum_bound(exact_weights, bounds)
|
|
188
|
+
if require_unique:
|
|
189
|
+
certificate.require_unique()
|
|
190
|
+
|
|
182
191
|
return WeightedInt32Result(
|
|
183
|
-
encoded=EncodedArray(
|
|
192
|
+
encoded=EncodedArray(r0, r1, r2, r3),
|
|
184
193
|
certificate=certificate,
|
|
185
194
|
output_shape=output_shape,
|
|
186
195
|
weights=exact_weights,
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: rns_engine
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.7.0
|
|
4
4
|
Summary: Exact RNS engine with session/cache API.
|
|
5
5
|
Author: Evan Wesley
|
|
6
6
|
License-Expression: AGPL-3.0-only
|
|
@@ -194,7 +194,7 @@ out = session.decode_signed(session.add(a, b))
|
|
|
194
194
|
|
|
195
195
|
## Weighted INT32 partial accumulation
|
|
196
196
|
|
|
197
|
-
Version 0.
|
|
197
|
+
Version 0.7 provides the fused direct bridge for digit-plane, chunked-GEMM, and Tensor Core pipelines.
|
|
198
198
|
|
|
199
199
|
Give the engine signed INT32 partial outputs with shape:
|
|
200
200
|
|
|
@@ -246,7 +246,7 @@ Important details:
|
|
|
246
246
|
- `decode_modular()` always returns the canonical residue in `[0, M)`.
|
|
247
247
|
- Empty term sets produce an exact zero result.
|
|
248
248
|
|
|
249
|
-
The current implementation
|
|
249
|
+
The current implementation uses one fused native `_weighted` call to read signed INT32 partials, collect exact per-term magnitude bounds, apply positional weights modulo `M`, and accumulate all four rails. The pre-fusion staged encode → scale → add body remains available internally as an exact reference for A/B tests. Local AVX2 diagnostics measured roughly 2.1×–2.9× speedup at one million outputs and larger gains on tiny many-term cases; those figures are hardware-specific CPU evidence, not CUDA, Tensor Core, or universal performance claims.
|
|
250
250
|
|
|
251
251
|
A precomputed receipt can also be created without materializing partial arrays:
|
|
252
252
|
|
|
@@ -404,9 +404,11 @@ rns.HAS_AVX2
|
|
|
404
404
|
|
|
405
405
|
## Current release
|
|
406
406
|
|
|
407
|
-
### v0.
|
|
407
|
+
### v0.7.0
|
|
408
408
|
|
|
409
|
-
- weighted signed INT32 partial accumulation
|
|
409
|
+
- fused native weighted signed INT32 partial accumulation
|
|
410
|
+
- one compiled pass for magnitude collection, positional weighting, and four-rail accumulation
|
|
411
|
+
- staged reference body retained for exact fused-versus-staged A/B benchmarking
|
|
410
412
|
- exact arbitrary-precision positional weights
|
|
411
413
|
- per-term magnitude receipts and strict no-wrap certification
|
|
412
414
|
- canonical modular and uniqueness-gated signed decode paths
|
|
@@ -4,6 +4,7 @@ pyproject.toml
|
|
|
4
4
|
setup.py
|
|
5
5
|
src/rns_engine/__init__.py
|
|
6
6
|
src/rns_engine/_core.cpp
|
|
7
|
+
src/rns_engine/_weighted.cpp
|
|
7
8
|
src/rns_engine/engine.py
|
|
8
9
|
src/rns_engine/signed.py
|
|
9
10
|
src/rns_engine/weighted.py
|
|
@@ -17,4 +18,5 @@ tests/test_packaging.py
|
|
|
17
18
|
tests/test_rns.py
|
|
18
19
|
tests/test_session.py
|
|
19
20
|
tests/test_signed.py
|
|
20
|
-
tests/test_weighted.py
|
|
21
|
+
tests/test_weighted.py
|
|
22
|
+
tests/test_weighted_benchmark.py
|
|
@@ -157,3 +157,53 @@ def test_weighted_bound_validates_lengths_and_nonnegative_term_bounds():
|
|
|
157
157
|
|
|
158
158
|
with pytest.raises(TypeError, match="addend_abs_bound"):
|
|
159
159
|
rns.certify_weighted_sum_bound([1], [1], addend_abs_bound=1.5)
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
def test_fused_weighted_kernel_matches_staged_reference_randomized():
|
|
163
|
+
from rns_engine.weighted import (
|
|
164
|
+
_accumulate_weighted_int32_staged,
|
|
165
|
+
_validate_weighted_inputs,
|
|
166
|
+
)
|
|
167
|
+
|
|
168
|
+
rng = np.random.default_rng(20260802)
|
|
169
|
+
for terms, outputs in [(0, 7), (1, 1), (2, 17), (5, 257), (9, 1024)]:
|
|
170
|
+
partials = rng.integers(
|
|
171
|
+
np.iinfo(np.int32).min,
|
|
172
|
+
np.iinfo(np.int32).max,
|
|
173
|
+
size=(terms, outputs),
|
|
174
|
+
dtype=np.int32,
|
|
175
|
+
)
|
|
176
|
+
weights = tuple(
|
|
177
|
+
((-1) ** position) * (2 ** (13 * position) + 17)
|
|
178
|
+
for position in range(terms)
|
|
179
|
+
)
|
|
180
|
+
|
|
181
|
+
fused = rns.accumulate_weighted_int32(partials, weights)
|
|
182
|
+
flat, exact_weights, output_shape, output_size, _ = _validate_weighted_inputs(
|
|
183
|
+
partials,
|
|
184
|
+
weights,
|
|
185
|
+
)
|
|
186
|
+
staged_rails, staged_bounds = _accumulate_weighted_int32_staged(
|
|
187
|
+
flat,
|
|
188
|
+
exact_weights,
|
|
189
|
+
output_size,
|
|
190
|
+
)
|
|
191
|
+
|
|
192
|
+
assert fused.output_shape == output_shape
|
|
193
|
+
assert fused.term_abs_bounds == staged_bounds
|
|
194
|
+
for fused_rail, staged_rail in zip(fused.encoded.rails(), staged_rails):
|
|
195
|
+
np.testing.assert_array_equal(fused_rail, staged_rail)
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
def test_fused_kernel_handles_noncontiguous_input_without_changing_result():
|
|
199
|
+
base = np.arange(4 * 20, dtype=np.int32).reshape(4, 20)
|
|
200
|
+
partials = base[:, ::2]
|
|
201
|
+
assert not partials.flags.c_contiguous
|
|
202
|
+
|
|
203
|
+
receipt = rns.accumulate_weighted_int32(partials, [1, -2, 3, -4])
|
|
204
|
+
expected = sum(
|
|
205
|
+
weight * term.astype(np.int64)
|
|
206
|
+
for weight, term in zip([1, -2, 3, -4], partials)
|
|
207
|
+
)
|
|
208
|
+
|
|
209
|
+
np.testing.assert_array_equal(receipt.decode_signed(), expected)
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import importlib.util
|
|
4
|
+
import sys
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
import numpy as np
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
BENCHMARK_PATH = (
|
|
11
|
+
Path(__file__).resolve().parents[1]
|
|
12
|
+
/ "benchmarks"
|
|
13
|
+
/ "weighted_int32_baseline.py"
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def _load_benchmark_module():
|
|
18
|
+
spec = importlib.util.spec_from_file_location(
|
|
19
|
+
"weighted_int32_baseline",
|
|
20
|
+
BENCHMARK_PATH,
|
|
21
|
+
)
|
|
22
|
+
if spec is None or spec.loader is None:
|
|
23
|
+
raise RuntimeError("could not load weighted INT32 benchmark module")
|
|
24
|
+
|
|
25
|
+
module = importlib.util.module_from_spec(spec)
|
|
26
|
+
# Dataclasses with postponed annotations resolve their defining module
|
|
27
|
+
# through sys.modules while the class decorator executes.
|
|
28
|
+
sys.modules[spec.name] = module
|
|
29
|
+
try:
|
|
30
|
+
spec.loader.exec_module(module)
|
|
31
|
+
except Exception:
|
|
32
|
+
sys.modules.pop(spec.name, None)
|
|
33
|
+
raise
|
|
34
|
+
return module
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def test_weight_families_are_deterministic():
|
|
38
|
+
benchmark = _load_benchmark_module()
|
|
39
|
+
|
|
40
|
+
assert benchmark._weights(4, "unit", 128) == (1, 1, 1, 1)
|
|
41
|
+
assert benchmark._weights(4, "alternating", 128) == (1, -1, 1, -1)
|
|
42
|
+
assert benchmark._weights(4, "radix", 8) == (1, 8, 64, 512)
|
|
43
|
+
assert benchmark._weights(4, "centered-radix", 8) == (1, -8, 64, -512)
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def test_tiny_benchmark_case_runs_with_exact_witnesses():
|
|
47
|
+
benchmark = _load_benchmark_module()
|
|
48
|
+
|
|
49
|
+
result = benchmark.run_case(
|
|
50
|
+
terms=2,
|
|
51
|
+
outputs=8,
|
|
52
|
+
max_abs=7,
|
|
53
|
+
weight_mode="alternating",
|
|
54
|
+
radix=128,
|
|
55
|
+
repeats=1,
|
|
56
|
+
warmups=0,
|
|
57
|
+
sample_count=8,
|
|
58
|
+
rng=np.random.default_rng(12345),
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
assert result.terms == 2
|
|
62
|
+
assert result.outputs == 8
|
|
63
|
+
assert result.fused_native_calls == 1
|
|
64
|
+
assert result.staged_native_calls == 7
|
|
65
|
+
assert result.fused_speedup_over_staged > 0
|
|
66
|
+
assert result.max_abs_bound <= 14
|
|
67
|
+
assert result.unique_signed_result is True
|
|
68
|
+
assert result.fused_median_seconds >= 0
|
|
69
|
+
assert result.staged_median_seconds >= 0
|
|
70
|
+
assert result.decode_median_seconds >= 0
|
|
71
|
+
assert result.numpy_int64_control_median_seconds is not None
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|