Adaptive Precision
Per-layer adaptive bitstream length and per-synapse bit-width planning for
mixed-precision SC networks.
assign_lengths — auto-select bitstream length per layer from
Hoeffding or sensitivity bounds.
assign_synapse_precisions — choose integer bit widths and stochastic
bitstream lengths per synapse.
auto_tune_synapse_precisions — emit the public precision-plan manifest for a
percent error target.
write_precision_formal_evidence_bundle — write bounded SVA/SBY/JSON evidence
scaffolding for a precision plan.
Pythonfrom sc_neurocore.compiler.adaptive_precision import assign_lengths
sc_neurocore.compiler.adaptive_precision is in the scoped public-docstring
policy. Its dedicated adaptive-precision tests are strict typed and cover
layer-length assignment, sensitivity analysis, per-synapse planning, manifest
stability, row validation, and formal-evidence bundle writing at 100% isolated
facade coverage.
LayerPrecision and SynapsePrecision are validated row contracts behind the
public manifest. Layer rows reject negative indices, empty names, non-positive
or non-power-of-two bitstream lengths, and non-finite or negative error and
sensitivity values. Synapse rows reject negative coordinates, empty layer names,
non-positive bit widths or bitstream lengths, invalid component bounds, and
total error bounds that do not cover the quantisation plus stochastic
components.
The layer and synapse planners now fail closed before row emission. assign_lengths
rejects unsupported methods, mismatched layer_names, invalid target/length
bounds, empty layers, non-finite weights, and tensors outside the documented
1D/2D contract. assign_synapse_precisions rejects invalid target, bit-width,
length, and confidence bounds, mismatched names or sensitivity maps, empty or
non-finite weight layers, and non-finite or negative sensitivity maps. One
dimensional layer and sensitivity vectors remain supported and are serialized as
single-output rows.
The polyglot adaptive-precision mirrors are validated with the Python row
contract:
- Rust:
src/sc_neurocore/accel/rust/safety/adaptive_precision.rs compiles as
a Rust test binary and exercises layer/synapse contract checks.
- Julia:
src/sc_neurocore/accel/julia/compiler/adaptive_precision.jl exposes
LayerPrecisionState, SynapsePrecisionState, to_dict, and validation
helpers.
- Mojo:
src/sc_neurocore/accel/mojo/kernels/adaptive_precision.mojo runs the
same layer and synapse validation smoke path.
Local non-isolated benchmark and validation evidence is stored in
benchmarks/results/bench_adaptive_precision_rows.json. On this workstation,
the 2026-06-27 artifact records 10,000 Python LayerPrecision row
constructor/serialization calls in 0.031964 s (312849.473 calls/s), 10,000
SynapsePrecision row constructor/serialization calls in 0.10749 s
(93031.712 calls/s), Rust compile status pass, Rust tests status pass,
Julia validation status pass, and Mojo validation status pass. The artifact
sets production_benchmark_claim to false; it is regression evidence, not a
production performance claim.
The 2026-06-27 planner validation slice did not change numeric planner
objectives or add a new backend benchmark. It added fail-closed validation and a
dedicated coverage gate:
tests/.coveragerc_adaptive_precision_planners, covering
length_planner.py and synapse_planner.py at 100% with the adaptive precision
planner tests.
2026-04-30 per-synapse precision plan
The adaptive precision module now includes a conservative per-synapse planner
for the roadmap auto-adaptive precision optimiser. It assigns integer
bit_width, SC bitstream_length, sensitivity, quantisation-error bound,
stochastic-error bound, and total bound for each synapse:
Pythonimport numpy as np
from sc_neurocore.compiler.adaptive_precision import (
assign_synapse_precisions,
precision_plan_manifest,
)
weights = [np.array([[0.1, 0.8], [0.0, 0.4]])]
plan = assign_synapse_precisions(weights, target_error=0.05)
manifest = precision_plan_manifest(plan)
This is a deterministic planning surface, not a training-result claim. Bounds
are intentionally conservative: quantisation is bounded by half an integer
step scaled by sensitivity, and stochastic sampling uses the existing Hoeffding
bitstream-length helper. Custom sensitivity maps can be supplied after an
external sensitivity-analysis pass.
compile_adaptive_precision(...) accepts fixed Q-format strings such as
Q8.8/Q16.16 and block-floating strings such as BFP16E3X32. When a
block-floating precision is supplied with lp_parameter_count or
hp_parameter_count, the generated manifest records the exact
block_exponent_layout: flattened row-major parameter count, block size,
exponent-vector length, and final partial-block size. Invalid negative
parameter counts fail before RTL emission.
This metadata is a compiler contract for downstream emitters. The generated
adaptive wrapper still emits fixed mantissa-width datapaths; shared exponents
remain explicit metadata until the target-specific BFP datapath is selected.
Every adaptive manifest carries adaptive_precision_emitter.v1, explicit
emitted_datapath_width, emitted_datapath_fraction,
exponent_stream_width, and exponent_vector_width fields. Fixed Q-format
paths set the exponent widths to zero and reject accidental
*_parameter_count inputs so a block-exponent layout cannot be silently
dropped before HDL/Rust emitter handoff.
sc_neurocore.compiler.adaptive_precision
Per-layer adaptive bitstream length and per-synapse bit width facade.
LayerPrecision
dataclass
Bitstream length assignment for one layer.
Parameters
layer_index:
Zero-based layer index in the adaptive-precision plan.
name:
Non-empty layer name used in reports and manifests.
bitstream_length:
Positive stochastic-computing bitstream length. Layer-level planners
round this value to a power of two.
error_bound:
Finite non-negative per-layer stochastic error bound.
sensitivity:
Finite non-negative sensitivity score used for budget allocation.
Source code in src/sc_neurocore/compiler/layer_precision.py
| Python |
|---|
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61 | @dataclass
class LayerPrecision:
"""Bitstream length assignment for one layer.
Parameters
----------
layer_index:
Zero-based layer index in the adaptive-precision plan.
name:
Non-empty layer name used in reports and manifests.
bitstream_length:
Positive stochastic-computing bitstream length. Layer-level planners
round this value to a power of two.
error_bound:
Finite non-negative per-layer stochastic error bound.
sensitivity:
Finite non-negative sensitivity score used for budget allocation.
"""
layer_index: int
name: str
bitstream_length: int
error_bound: float
sensitivity: float
def __post_init__(self) -> None:
"""Validate adaptive-precision row invariants."""
_validate_non_negative_int(self.layer_index, "layer_index")
if not isinstance(self.name, str) or not self.name:
raise ValueError("name must be a non-empty string")
_validate_positive_int(self.bitstream_length, "bitstream_length")
if self.bitstream_length & (self.bitstream_length - 1) != 0:
raise ValueError("bitstream_length must be a power of two")
_validate_non_negative_float(self.error_bound, "error_bound")
_validate_non_negative_float(self.sensitivity, "sensitivity")
def to_dict(self) -> dict[str, int | float | str]:
"""Return a JSON-serializable adaptive-precision manifest row."""
return {
"layer_index": self.layer_index,
"name": self.name,
"bitstream_length": self.bitstream_length,
"error_bound": self.error_bound,
"sensitivity": self.sensitivity,
}
|
__post_init__()
Validate adaptive-precision row invariants.
Source code in src/sc_neurocore/compiler/layer_precision.py
| Python |
|---|
42
43
44
45
46
47
48
49
50
51 | def __post_init__(self) -> None:
"""Validate adaptive-precision row invariants."""
_validate_non_negative_int(self.layer_index, "layer_index")
if not isinstance(self.name, str) or not self.name:
raise ValueError("name must be a non-empty string")
_validate_positive_int(self.bitstream_length, "bitstream_length")
if self.bitstream_length & (self.bitstream_length - 1) != 0:
raise ValueError("bitstream_length must be a power of two")
_validate_non_negative_float(self.error_bound, "error_bound")
_validate_non_negative_float(self.sensitivity, "sensitivity")
|
to_dict()
Return a JSON-serializable adaptive-precision manifest row.
Source code in src/sc_neurocore/compiler/layer_precision.py
| Python |
|---|
53
54
55
56
57
58
59
60
61 | def to_dict(self) -> dict[str, int | float | str]:
"""Return a JSON-serializable adaptive-precision manifest row."""
return {
"layer_index": self.layer_index,
"name": self.name,
"bitstream_length": self.bitstream_length,
"error_bound": self.error_bound,
"sensitivity": self.sensitivity,
}
|
SynapsePrecision
dataclass
Precision assignment and conservative error bound for one synapse.
Parameters
layer_index:
Zero-based layer index in the adaptive-precision plan.
layer_name:
Non-empty layer name used in reports and manifests.
output_index:
Zero-based output row index for the weight matrix.
input_index:
Zero-based input column index for the weight matrix.
bit_width:
Positive fixed-point bit width assigned to the synapse.
bitstream_length:
Positive stochastic-computing bitstream length assigned to the synapse.
sensitivity:
Finite non-negative synapse sensitivity score.
quantization_error_bound:
Finite non-negative error bound from fixed-point quantization.
stochastic_error_bound:
Finite non-negative Hoeffding-style stochastic error bound.
total_error_bound:
Finite non-negative aggregate bound that must cover both components.
Source code in src/sc_neurocore/compiler/synapse_precision.py
| Python |
|---|
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92 | @dataclass(frozen=True)
class SynapsePrecision:
"""Precision assignment and conservative error bound for one synapse.
Parameters
----------
layer_index:
Zero-based layer index in the adaptive-precision plan.
layer_name:
Non-empty layer name used in reports and manifests.
output_index:
Zero-based output row index for the weight matrix.
input_index:
Zero-based input column index for the weight matrix.
bit_width:
Positive fixed-point bit width assigned to the synapse.
bitstream_length:
Positive stochastic-computing bitstream length assigned to the synapse.
sensitivity:
Finite non-negative synapse sensitivity score.
quantization_error_bound:
Finite non-negative error bound from fixed-point quantization.
stochastic_error_bound:
Finite non-negative Hoeffding-style stochastic error bound.
total_error_bound:
Finite non-negative aggregate bound that must cover both components.
"""
layer_index: int
layer_name: str
output_index: int
input_index: int
bit_width: int
bitstream_length: int
sensitivity: float
quantization_error_bound: float
stochastic_error_bound: float
total_error_bound: float
def __post_init__(self) -> None:
"""Validate per-synapse precision-row invariants."""
_validate_non_negative_int(self.layer_index, "layer_index")
if not isinstance(self.layer_name, str) or not self.layer_name:
raise ValueError("layer_name must be a non-empty string")
_validate_non_negative_int(self.output_index, "output_index")
_validate_non_negative_int(self.input_index, "input_index")
_validate_positive_int(self.bit_width, "bit_width")
_validate_positive_int(self.bitstream_length, "bitstream_length")
_validate_non_negative_float(self.sensitivity, "sensitivity")
_validate_non_negative_float(
self.quantization_error_bound,
"quantization_error_bound",
)
_validate_non_negative_float(
self.stochastic_error_bound,
"stochastic_error_bound",
)
_validate_non_negative_float(self.total_error_bound, "total_error_bound")
component_sum = self.quantization_error_bound + self.stochastic_error_bound
if self.total_error_bound + 1e-15 < component_sum:
raise ValueError("total_error_bound must cover quantization and stochastic bounds")
def to_dict(self) -> dict[str, int | float | str]:
"""Return a JSON-serialisable precision-plan row."""
return {
"layer_index": self.layer_index,
"layer_name": self.layer_name,
"output_index": self.output_index,
"input_index": self.input_index,
"bit_width": self.bit_width,
"bitstream_length": self.bitstream_length,
"sensitivity": self.sensitivity,
"quantization_error_bound": self.quantization_error_bound,
"stochastic_error_bound": self.stochastic_error_bound,
"total_error_bound": self.total_error_bound,
}
|
__post_init__()
Validate per-synapse precision-row invariants.
Source code in src/sc_neurocore/compiler/synapse_precision.py
| Python |
|---|
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77 | def __post_init__(self) -> None:
"""Validate per-synapse precision-row invariants."""
_validate_non_negative_int(self.layer_index, "layer_index")
if not isinstance(self.layer_name, str) or not self.layer_name:
raise ValueError("layer_name must be a non-empty string")
_validate_non_negative_int(self.output_index, "output_index")
_validate_non_negative_int(self.input_index, "input_index")
_validate_positive_int(self.bit_width, "bit_width")
_validate_positive_int(self.bitstream_length, "bitstream_length")
_validate_non_negative_float(self.sensitivity, "sensitivity")
_validate_non_negative_float(
self.quantization_error_bound,
"quantization_error_bound",
)
_validate_non_negative_float(
self.stochastic_error_bound,
"stochastic_error_bound",
)
_validate_non_negative_float(self.total_error_bound, "total_error_bound")
component_sum = self.quantization_error_bound + self.stochastic_error_bound
if self.total_error_bound + 1e-15 < component_sum:
raise ValueError("total_error_bound must cover quantization and stochastic bounds")
|
to_dict()
Return a JSON-serialisable precision-plan row.
Source code in src/sc_neurocore/compiler/synapse_precision.py
| Python |
|---|
79
80
81
82
83
84
85
86
87
88
89
90
91
92 | def to_dict(self) -> dict[str, int | float | str]:
"""Return a JSON-serialisable precision-plan row."""
return {
"layer_index": self.layer_index,
"layer_name": self.layer_name,
"output_index": self.output_index,
"input_index": self.input_index,
"bit_width": self.bit_width,
"bitstream_length": self.bitstream_length,
"sensitivity": self.sensitivity,
"quantization_error_bound": self.quantization_error_bound,
"stochastic_error_bound": self.stochastic_error_bound,
"total_error_bound": self.total_error_bound,
}
|
auto_tune_synapse_precisions(layer_weights, *, layer_names=None, target_error_percent=0.1, min_bits=4, max_bits=16, min_length=32, max_length=4096, confidence=0.95)
Auto-tune per-synapse precision for an explicit percent error target.
Source code in src/sc_neurocore/compiler/auto_tune.py
| Python |
|---|
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75 | def auto_tune_synapse_precisions(
layer_weights: list[np.ndarray[Any, Any]],
*,
layer_names: list[str] | None = None,
target_error_percent: float = 0.1,
min_bits: int = 4,
max_bits: int = 16,
min_length: int = 32,
max_length: int = 4096,
confidence: float = 0.95,
) -> dict[str, Any]:
"""Auto-tune per-synapse precision for an explicit percent error target."""
if target_error_percent <= 0:
raise ValueError("target_error_percent must be positive")
target_error = target_error_percent / 100.0
assignments = assign_synapse_precisions(
layer_weights,
layer_names=layer_names,
target_error=target_error,
min_bits=min_bits,
max_bits=max_bits,
min_length=min_length,
max_length=max_length,
confidence=confidence,
)
manifest = precision_plan_manifest(assignments)
manifest["api_surface"] = {
"action_id": "auto_tune_adaptive_precision",
"target_error_percent": target_error_percent,
"target_error_fraction": target_error,
"objective": "minimal_luts_under_error_target",
"cost_metric": "sum(bit_width * log2(bitstream_length))",
"estimated_lut_cost": manifest["cost_summary"]["estimated_lut_cost"],
"uniform_length_reference_cost": manifest["cost_summary"]["uniform_length_reference_cost"],
"estimated_lut_savings_vs_uniform_length": manifest["cost_summary"][
"estimated_lut_savings_vs_uniform_length"
],
}
return manifest
|
precision_plan_manifest(assignments)
Build a deterministic manifest for a per-synapse precision plan.
Source code in src/sc_neurocore/compiler/auto_tune.py
| Python |
|---|
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34 | def precision_plan_manifest(assignments: list[Any]) -> dict[str, Any]:
"""Build a deterministic manifest for a per-synapse precision plan."""
rows = [assignment.to_dict() for assignment in assignments]
cost_summary = _precision_cost_summary(assignments)
return {
"schema": "sc-neurocore.adaptive_precision_plan.v1",
"granularity": "synapse",
"num_synapses": len(assignments),
"max_total_error_bound": max(
(assignment.total_error_bound for assignment in assignments),
default=0.0,
),
"cost_summary": cost_summary,
"assignments": rows,
}
|
Write a deterministic SymbiYosys evidence bundle for precision claims.
Source code in src/sc_neurocore/compiler/formal_evidence.py
| Python |
|---|
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84 | def write_precision_formal_evidence_bundle(
output_dir: str | Path,
assignments: list[SynapsePrecision],
*,
module_name: str = "adaptive_precision_plan",
) -> dict[str, Any]:
"""Write a deterministic SymbiYosys evidence bundle for precision claims."""
if not assignments:
raise ValueError("assignments must not be empty")
out = Path(output_dir)
out.mkdir(parents=True, exist_ok=True)
max_error = max(item.total_error_bound for item in assignments)
max_bits = max(item.bit_width for item in assignments)
max_length = max(item.bitstream_length for item in assignments)
rtl_file = f"{module_name}.v"
sva_file = f"{module_name}_sva.sv"
sby_file = f"{module_name}.sby"
report_file = f"{module_name}_formal_report.json"
(out / sva_file).write_text(
_adaptive_precision_sva(
module_name=module_name,
max_total_error_bound=max_error,
max_bit_width=max_bits,
max_bitstream_length=max_length,
),
encoding="utf-8",
)
from .deployment import generate_sby_script
(out / sby_file).write_text(
generate_sby_script(module_name, sva_file=sva_file, mode="prove", depth=32),
encoding="utf-8",
)
manifest = {
"schema_version": "sc-neurocore.adaptive-precision-formal-bundle.v1",
"evidence_classification": "compile",
"status": "completed",
"module_name": module_name,
"evidence_boundary": ("bundle_generation_only_no_symbiyosys_execution_no_silicon_claim"),
"assignments_count": len(assignments),
"formal_claim": {
"max_total_error_bound": max_error,
"max_bit_width": max_bits,
"max_bitstream_length": max_length,
"symbiyosys_executed": False,
"formal_proof_passed": False,
"hardware_measurement_claimed": False,
},
"artifacts": {
"rtl": rtl_file,
"sva": sva_file,
"sby": sby_file,
"report": report_file,
},
}
(out / f"{module_name}_formal_manifest.json").write_text(
json.dumps(manifest, indent=2, sort_keys=True) + "\n",
encoding="utf-8",
)
return manifest
|
assign_lengths(layer_weights, layer_names=None, total_budget=None, min_length=32, max_length=1024, target_error=0.01, method='hoeffding')
Assign per-layer bitstream lengths under a target error budget.
Parameters
layer_weights:
One- or two-dimensional finite weight tensors, one tensor per layer.
One-dimensional tensors are treated as single-output layers.
layer_names:
Optional non-empty layer names. When provided, the list length must match
layer_weights exactly.
total_budget:
Optional aggregate bitstream-length budget for sensitivity planning.
When omitted, sensitivity planning uses max_length * n_layers.
min_length:
Minimum bitstream length assigned to any layer.
max_length:
Maximum bitstream length assigned to any layer.
target_error:
Positive per-layer target error used by the Hoeffding planner.
method:
Planning method. hoeffding uses analytic Hoeffding lengths;
sensitivity and proportional allocate from sensitivity scores.
Returns
list[LayerPrecision]
Validated layer precision rows in input-layer order.
Raises
ValueError
If planner bounds, method, names, or weight tensors are invalid.
Source code in src/sc_neurocore/compiler/length_planner.py
| Python |
|---|
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123 | def assign_lengths(
layer_weights: list[np.ndarray[Any, Any]],
layer_names: list[str] | None = None,
total_budget: int | None = None,
min_length: int = 32,
max_length: int = 1024,
target_error: float = 0.01,
method: str = "hoeffding",
) -> list[LayerPrecision]:
"""Assign per-layer bitstream lengths under a target error budget.
Parameters
----------
layer_weights:
One- or two-dimensional finite weight tensors, one tensor per layer.
One-dimensional tensors are treated as single-output layers.
layer_names:
Optional non-empty layer names. When provided, the list length must match
`layer_weights` exactly.
total_budget:
Optional aggregate bitstream-length budget for sensitivity planning.
When omitted, sensitivity planning uses `max_length * n_layers`.
min_length:
Minimum bitstream length assigned to any layer.
max_length:
Maximum bitstream length assigned to any layer.
target_error:
Positive per-layer target error used by the Hoeffding planner.
method:
Planning method. `hoeffding` uses analytic Hoeffding lengths;
`sensitivity` and `proportional` allocate from sensitivity scores.
Returns
-------
list[LayerPrecision]
Validated layer precision rows in input-layer order.
Raises
------
ValueError
If planner bounds, method, names, or weight tensors are invalid.
"""
_validate_planner_bounds(min_length, max_length, target_error)
if method not in {"hoeffding", "sensitivity", "proportional"}:
raise ValueError("method must be one of: hoeffding, sensitivity, proportional")
validated_weights = [_as_weight_array(weights) for weights in layer_weights]
n_layers = len(validated_weights)
if layer_names is None:
layer_names = [f"layer_{i}" for i in range(n_layers)]
elif len(layer_names) != n_layers:
raise ValueError("layer_names length must match layer_weights")
if method == "hoeffding":
assignments: list[LayerPrecision] = []
for i, (w, name) in enumerate(zip(validated_weights, layer_names)):
fan_in = w.shape[1] if w.ndim == 2 else 1
per_syn_eps = target_error / max(1, np.sqrt(fan_in))
L = adaptive_length(p=0.5, epsilon=per_syn_eps, confidence=0.95)
L = int(np.clip(L, min_length, max_length))
L = int(2 ** np.ceil(np.log2(max(L, min_length))))
L = min(L, max_length)
bound = 0.5 / np.sqrt(L) if L > 0 else 1.0
assignments.append(
LayerPrecision(
layer_index=i,
name=name,
bitstream_length=L,
error_bound=bound,
sensitivity=0.0,
)
)
return assignments
sensitivities = analyze_sensitivity(validated_weights)
total_sens = sum(sensitivities) or 1.0
if total_budget is None:
total_budget = max_length * n_layers
elif total_budget <= 0:
raise ValueError("total_budget must be positive when provided")
assignments = []
for i, (w, name, sens) in enumerate(zip(validated_weights, layer_names, sensitivities)):
fraction = sens / total_sens
L = int(fraction * total_budget / n_layers * n_layers)
L = int(np.clip(L, min_length, max_length))
L = int(2 ** np.ceil(np.log2(max(L, min_length))))
L = min(L, max_length)
bound = 0.5 / np.sqrt(L) if L > 0 else 1.0
assignments.append(
LayerPrecision(
layer_index=i,
name=name,
bitstream_length=L,
error_bound=bound,
sensitivity=sens,
)
)
return assignments
|
analyze_sensitivity(layer_weights, lengths=None, n_trials=100, seed=42)
Measure per-layer sensitivity to bitstream length reduction.
Parameters
layer_weights:
One-dimensional or two-dimensional layer weight arrays. Vector weights
are treated as a single-output dense layer.
lengths:
Candidate stochastic bitstream lengths to sample. When omitted, the
estimator uses the default production planning ladder.
n_trials:
Number of independent input-vector samples per layer.
seed:
Deterministic NumPy random seed used for reproducible planning.
Returns
list[float]
One non-negative sensitivity score for each supplied layer.
Raises
ValueError
If trial count, candidate lengths, or layer weight arrays are invalid.
Source code in src/sc_neurocore/compiler/sensitivity_analysis.py
| Python |
|---|
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101 | def analyze_sensitivity(
layer_weights: list[np.ndarray[Any, Any]],
lengths: list[int] | None = None,
n_trials: int = 100,
seed: int = 42,
) -> list[float]:
"""Measure per-layer sensitivity to bitstream length reduction.
Parameters
----------
layer_weights:
One-dimensional or two-dimensional layer weight arrays. Vector weights
are treated as a single-output dense layer.
lengths:
Candidate stochastic bitstream lengths to sample. When omitted, the
estimator uses the default production planning ladder.
n_trials:
Number of independent input-vector samples per layer.
seed:
Deterministic NumPy random seed used for reproducible planning.
Returns
-------
list[float]
One non-negative sensitivity score for each supplied layer.
Raises
------
ValueError
If trial count, candidate lengths, or layer weight arrays are invalid.
"""
candidate_lengths = _validate_lengths(lengths)
if n_trials <= 0:
raise ValueError("n_trials must be positive")
rng = np.random.RandomState(seed)
sensitivities: list[float] = []
for raw_weights in layer_weights:
weights = _as_weight_matrix(raw_weights)
n_outputs, n_inputs = weights.shape
clipped_weights = np.clip(weights, 0.0, 1.0)
errors: list[float] = []
for _ in range(n_trials):
input_probabilities = rng.random_sample(n_inputs).astype(np.float64)
exact = weights @ input_probabilities
target = np.clip(exact, 0.0, None)
length_errors: list[float] = []
for bitstream_length in candidate_lengths:
sc_results: list[_FloatArray] = []
for _ in range(5):
bits_x = (
rng.random_sample((bitstream_length, n_inputs)) < input_probabilities
).astype(np.float64)
bits_w = (
rng.random_sample((bitstream_length, n_outputs, n_inputs))
< clipped_weights[np.newaxis, :, :]
).astype(np.float64)
and_result = bits_x[:, np.newaxis, :] * bits_w
counts = np.sum(
and_result,
axis=(0, 2),
dtype=np.float64,
initial=0.0,
)
sc_results.append(
np.asarray(counts / float(bitstream_length), dtype=np.float64)
)
sc_mean = np.mean(np.stack(sc_results, axis=0), axis=0)
err = np.mean(np.abs(sc_mean - target))
length_errors.append(float(err))
sensitivity = max(length_errors) - min(length_errors) if length_errors else 0.0
errors.append(sensitivity)
sensitivities.append(float(np.mean(errors)))
return sensitivities
|
assign_synapse_precisions(layer_weights, layer_names=None, sensitivity_maps=None, target_error=0.01, min_bits=4, max_bits=16, min_length=32, max_length=4096, confidence=0.95)
Assign per-synapse bit widths and SC lengths with error bounds.
Parameters
layer_weights:
One- or two-dimensional finite weight tensors, one tensor per layer.
One-dimensional tensors are treated as single-output layers.
layer_names:
Optional non-empty layer names. When provided, the list length must match
layer_weights exactly.
sensitivity_maps:
Optional finite non-negative sensitivity maps with shapes matching each
corresponding weight tensor.
target_error:
Positive aggregate target error fraction.
min_bits:
Minimum fixed-point bit width assigned to any synapse.
max_bits:
Maximum fixed-point bit width assigned to any synapse.
min_length:
Minimum stochastic bitstream length assigned to any synapse.
max_length:
Maximum stochastic bitstream length assigned to any synapse.
confidence:
Hoeffding confidence in the open interval (0, 1).
Returns
list[SynapsePrecision]
Validated per-synapse precision rows in layer/output/input order.
Raises
ValueError
If planner bounds, names, sensitivity maps, or weight tensors are
invalid.
Source code in src/sc_neurocore/compiler/synapse_planner.py
| Python |
|---|
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195 | def assign_synapse_precisions(
layer_weights: list[np.ndarray[Any, Any]],
layer_names: list[str] | None = None,
sensitivity_maps: list[np.ndarray[Any, Any]] | None = None,
target_error: float = 0.01,
min_bits: int = 4,
max_bits: int = 16,
min_length: int = 32,
max_length: int = 4096,
confidence: float = 0.95,
) -> list[SynapsePrecision]:
"""Assign per-synapse bit widths and SC lengths with error bounds.
Parameters
----------
layer_weights:
One- or two-dimensional finite weight tensors, one tensor per layer.
One-dimensional tensors are treated as single-output layers.
layer_names:
Optional non-empty layer names. When provided, the list length must match
`layer_weights` exactly.
sensitivity_maps:
Optional finite non-negative sensitivity maps with shapes matching each
corresponding weight tensor.
target_error:
Positive aggregate target error fraction.
min_bits:
Minimum fixed-point bit width assigned to any synapse.
max_bits:
Maximum fixed-point bit width assigned to any synapse.
min_length:
Minimum stochastic bitstream length assigned to any synapse.
max_length:
Maximum stochastic bitstream length assigned to any synapse.
confidence:
Hoeffding confidence in the open interval `(0, 1)`.
Returns
-------
list[SynapsePrecision]
Validated per-synapse precision rows in layer/output/input order.
Raises
------
ValueError
If planner bounds, names, sensitivity maps, or weight tensors are
invalid.
"""
if not np.isfinite(target_error) or target_error <= 0.0:
raise ValueError("target_error must be finite and positive")
if min_bits < 1 or max_bits < min_bits:
raise ValueError("bit-width bounds must satisfy 1 <= min_bits <= max_bits")
if min_length < 1 or max_length < min_length:
raise ValueError("length bounds must satisfy 1 <= min_length <= max_length")
if confidence <= 0.0 or confidence >= 1.0:
raise ValueError("confidence must satisfy 0.0 < confidence < 1.0")
validated_weights = [_as_weight_array(weights) for weights in layer_weights]
n_layers = len(validated_weights)
if layer_names is None:
layer_names = [f"layer_{i}" for i in range(n_layers)]
if len(layer_names) != n_layers:
raise ValueError("layer_names length must match layer_weights")
if sensitivity_maps is not None and len(sensitivity_maps) != n_layers:
raise ValueError("sensitivity_maps length must match layer_weights")
total_synapses = sum(int(w.size) for w in validated_weights)
local_target = target_error / max(1.0, float(np.sqrt(total_synapses)))
assignments: list[SynapsePrecision] = []
for layer_index, (w, name) in enumerate(zip(validated_weights, layer_names)):
matrix: np.ndarray[Any, Any] = w.reshape(1, -1) if w.ndim == 1 else w
if sensitivity_maps is None:
max_abs = float(np.max(np.abs(matrix))) if matrix.size else 0.0
sensitivity = np.abs(matrix) / max(max_abs, 1e-12)
else:
sensitivity_raw = np.asarray(sensitivity_maps[layer_index], dtype=float)
if sensitivity_raw.shape != w.shape:
raise ValueError("each sensitivity map must match its layer weight shape")
sensitivity = (
sensitivity_raw.reshape(1, -1) if sensitivity_raw.ndim == 1 else sensitivity_raw
)
if np.any(sensitivity < 0) or not np.all(np.isfinite(sensitivity)):
raise ValueError("sensitivity maps must contain finite non-negative values")
for output_index in range(matrix.shape[0]):
for input_index in range(matrix.shape[1]):
sens = float(sensitivity[output_index, input_index])
bit_width = _select_bit_width(sens, local_target, min_bits, max_bits)
quant_bound = _quantization_error_bound(sens, bit_width)
remaining = max(local_target - quant_bound, local_target * 0.25)
if sens <= 0:
length = min_length
stochastic_bound = 0.0
else:
epsilon = max(remaining / sens, 1e-12)
length = adaptive_length(
p=0.5,
epsilon=epsilon,
confidence=confidence,
min_length=min_length,
max_length=max_length,
)
stochastic_bound = sens * _hoeffding_radius(length, confidence)
assignments.append(
SynapsePrecision(
layer_index=layer_index,
layer_name=name,
output_index=output_index,
input_index=input_index,
bit_width=bit_width,
bitstream_length=length,
sensitivity=sens,
quantization_error_bound=quant_bound,
stochastic_error_bound=stochastic_bound,
total_error_bound=quant_bound + stochastic_bound,
)
)
return assignments
|