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.
Files changed (24) hide show
  1. {rns_engine-0.6.0/src/rns_engine.egg-info → rns_engine-0.7.0}/PKG-INFO +7 -5
  2. {rns_engine-0.6.0 → rns_engine-0.7.0}/README.md +6 -4
  3. {rns_engine-0.6.0 → rns_engine-0.7.0}/pyproject.toml +1 -1
  4. {rns_engine-0.6.0 → rns_engine-0.7.0}/setup.py +16 -8
  5. rns_engine-0.7.0/src/rns_engine/_weighted.cpp +141 -0
  6. {rns_engine-0.6.0 → rns_engine-0.7.0}/src/rns_engine/weighted.py +47 -38
  7. {rns_engine-0.6.0 → rns_engine-0.7.0/src/rns_engine.egg-info}/PKG-INFO +7 -5
  8. {rns_engine-0.6.0 → rns_engine-0.7.0}/src/rns_engine.egg-info/SOURCES.txt +3 -1
  9. {rns_engine-0.6.0 → rns_engine-0.7.0}/tests/test_weighted.py +50 -0
  10. rns_engine-0.7.0/tests/test_weighted_benchmark.py +71 -0
  11. {rns_engine-0.6.0 → rns_engine-0.7.0}/LICENSE +0 -0
  12. {rns_engine-0.6.0 → rns_engine-0.7.0}/setup.cfg +0 -0
  13. {rns_engine-0.6.0 → rns_engine-0.7.0}/src/rns_engine/__init__.py +0 -0
  14. {rns_engine-0.6.0 → rns_engine-0.7.0}/src/rns_engine/_core.cpp +0 -0
  15. {rns_engine-0.6.0 → rns_engine-0.7.0}/src/rns_engine/engine.py +0 -0
  16. {rns_engine-0.6.0 → rns_engine-0.7.0}/src/rns_engine/signed.py +0 -0
  17. {rns_engine-0.6.0 → rns_engine-0.7.0}/src/rns_engine.egg-info/dependency_links.txt +0 -0
  18. {rns_engine-0.6.0 → rns_engine-0.7.0}/src/rns_engine.egg-info/not-zip-safe +0 -0
  19. {rns_engine-0.6.0 → rns_engine-0.7.0}/src/rns_engine.egg-info/requires.txt +0 -0
  20. {rns_engine-0.6.0 → rns_engine-0.7.0}/src/rns_engine.egg-info/top_level.txt +0 -0
  21. {rns_engine-0.6.0 → rns_engine-0.7.0}/tests/test_packaging.py +0 -0
  22. {rns_engine-0.6.0 → rns_engine-0.7.0}/tests/test_rns.py +0 -0
  23. {rns_engine-0.6.0 → rns_engine-0.7.0}/tests/test_session.py +0 -0
  24. {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.6.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.6 adds the first direct bridge for digit-plane, chunked-GEMM, and Tensor Core pipelines.
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 is an evidence-first baseline. Python schedules terms, while each per-element encode, scalar-weight multiply, and rail addition runs through the compiled native kernels. It is **not yet one fused C++ weighted-accumulation kernel**, and no fused-kernel speed claim is made.
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.6.0
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.6 adds the first direct bridge for digit-plane, chunked-GEMM, and Tensor Core pipelines.
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 is an evidence-first baseline. Python schedules terms, while each per-element encode, scalar-weight multiply, and rail addition runs through the compiled native kernels. It is **not yet one fused C++ weighted-accumulation kernel**, and no fused-kernel speed claim is made.
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.6.0
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
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "rns_engine"
7
- version = "0.6.0"
7
+ version = "0.7.0"
8
8
  description = "Exact RNS engine with session/cache API."
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.10"
@@ -39,19 +39,27 @@ def get_link_args():
39
39
  return []
40
40
 
41
41
 
42
- ext = Extension(
43
- "rns_engine._core",
44
- sources=["src/rns_engine/_core.cpp"],
45
- include_dirs=[pybind11.get_include(), np.get_include()],
46
- extra_compile_args=get_compile_args(),
47
- extra_link_args=get_link_args(),
48
- language="c++",
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 accumulate_weighted_int32(
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(*accumulator),
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.6.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.6 adds the first direct bridge for digit-plane, chunked-GEMM, and Tensor Core pipelines.
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 is an evidence-first baseline. Python schedules terms, while each per-element encode, scalar-weight multiply, and rail addition runs through the compiled native kernels. It is **not yet one fused C++ weighted-accumulation kernel**, and no fused-kernel speed claim is made.
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.6.0
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