Quantized Dense Layer — Machine Learning/Neural Networks
Machine_Learning/Neural_Networks/Quantized_Dense_Layer · 1 input / 1 output port(s) at insert · exports to Python, MATLAB, Java, Rust, C, C++, VHDL, Verilog, SystemVerilog, PLC Structured Text
Description#
The block's own DESCRIPTION_HTML, rendered verbatim — the same text the config dialog's info panel and the library navigator show. Fix a wrong sentence in the block's .cpp (R-D9), never here.
Quantized Dense Layer
Machine Learning / Neural Networks
A dense layer evaluated the way a deployed int8 network evaluates it: the input is quantized to an 8-bit code, the products are accumulated as integers, and the result is requantized and scaled back to a real signal.
It is lossy on purpose. That is the whole reason to choose it over Dense Layer, which multiplies doubles: the question this block answers is whether a network still behaves once its weights are eight bits and its activations saturate – and on the PLC and HDL targets, whether it fits on the part at all.
The arithmetic, in order:
- nᵢ = clamp(round(uᵢ / Su), −128−Zu, 127−Zu) – the int8 input code.
- accᵢ = Bᵢ + Σ⫺ (Wᵢ⫺ − Zw) · n⫺ – an integer accumulator.
- yᵢ = Sy · clamp(round(Mᵢ·accᵢ), −128−Zy, 127−Zy), with Mᵢ = Su·Swᵢ/Sy – requantize, then back to a real signal.
Ports
- u – the real feature column, [m,1], where m is the NUMBER OF COLUMNS of Weights (int8).
- Output – y, a column [p,1], where p is the NUMBER OF ROWS of Weights (int8). It is a real signal again, and it takes only the 255 distinct values the output quantization allows.
Parameters
- Weights (int8) – [p,m], one output per ROW, and every entry a whole number in [−128, 127]. These are the quantized weights exactly as a framework exports them – not the real-valued ones, which belong in Dense Layer.
- Weight Zero Point – Zw, a whole number in [−128, 127]. Symmetric int8 weights – what TensorFlow Lite and PyTorch both produce – have Zw = 0, which is the default; the parameter exists for the asymmetric case.
- Weight Scale – Sw, strictly positive: either one scale for the whole tensor (a scalar) or one per output row ([p,1], which is per-channel quantization, the modern default).
- Bias (int32) – [p,1], whole numbers, expressed in accumulator units rather than in the output's units. That is how a quantized graph stores it: the bias is pre-scaled at Su·Swᵢ, so it adds straight into the integer accumulator with no conversion.
- Input Scale / Input Zero Point – Su (> 0) and Zu, the quantization of the incoming signal. Su is the size of one input code step, so it sets what range of input the layer can see before it saturates.
- Output Scale / Output Zero Point – Sy (> 0) and Zy, the quantization of the result. Sy is the size of one output step, and therefore the resolution of everything downstream.
- Sampling Time (s) – zero or less inherits the solver's rate; a positive value runs the block at that period.
Code export
All ten targets: Python, MATLAB, Java, Rust, C, C++, VHDL, Verilog, SystemVerilog and PLC Structured Text. Every parameter is structural and baked in; there is no tunable parameter, and the layer is fully unrolled.
Rounding is half away from zero in all ten – the convention
Quantizer, MATLAB and TensorFlow Lite's reference kernels all use.
Python and Java get an explicit form because np.round rounds
halves to even and Math.round is a floor on a long. The zero
points do not appear in the emitted arithmetic at all: they fold into the
clamp bounds, exactly and not approximately.
The three HDL targets divide the accumulator by a power of two, because the integer accumulator does not fit: Q16.16 saturates at ±32768 and a single int8 product already reaches 127×255 = 32385. Each weight and the bias are divided by K, the smallest power of two that brings this layer's worst case under that ceiling, and Mᵢ·K is applied once at the end. Because K is a power of two, every divided weight, every product and the whole sum stay exact in Q16.16 – a small layer gets K = 1 and is bit-exact. There is still exactly one multiplier per weight and no divider anywhere.
What remains is that the input arrives already quantized to Q16.16, so a sample sitting within one quantum of a rounding tie can round the other way and shift that output by one Sy. That is inherent to a discontinuous output in fixed point – the same exposure Quantizer and Argmax Decision carry – rather than a codegen defect.
Simulink bridge
None. Simulink's Deep Learning blocks take a trained network
object, and its fixed-point tooling quantizes a model rather than
accepting an already-quantized one as parameters. The bridge reports the block
rather than dropping it silently, and it has no parity testbench, which
is the documented consequence of Support::None rather than a
gap.
Notes
- Algebraic and stateless: the output depends only on the current input, so the block cannot break an algebraic loop.
- Nonlinear and discontinuous – rounding and saturation both are – and it deliberately carries no state space.
- Saturation is a result, not an error. An input beyond ±128·Su clamps, and so does an output beyond its own range; that is what the deployed network does, and seeing it here is the point of simulating the quantized form rather than the exact one.
- The output takes 255 distinct values. If a downstream block needs finer resolution than Sy, the answer is a smaller Sy or an unquantized Dense Layer, not a filter.
Code facts#
| Fact | Value |
|---|---|
| registered type | Machine_Learning/Neural_Networks/Quantized_Dense_Layer |
| family | Machine_Learning/Neural_Networks |
| solver environment class | ICoreBlock_0_Machine_Learning_1_Neural_Networks_2_Quantized_Dense_Layer |
| source | src/ICoreSDK/ICoreBlockLibrary/Blocks/Machine_Learning/Neural_Networks/Quantized_Dense_Layer/ICoreBlock_0_Machine_Learning_1_Neural_Networks_2_Quantized_Dense_Layer.cpp |
| header | src/ICoreSDK/ICoreBlockLibrary/Blocks/Machine_Learning/Neural_Networks/Quantized_Dense_Layer/ICoreBlock_0_Machine_Learning_1_Neural_Networks_2_Quantized_Dense_Layer.h |
| default size on canvas | 140 × 84 px |
| ports at insert | 1 in, 1 out |
| code generators implemented | Python, MATLAB, Java, Rust, C, C++, VHDL, Verilog, SystemVerilog, PLC Structured Text |
Ports#
| # | Direction | Signal type | Description label |
|---|---|---|---|
| 1 | in | ICoreDouble | u |
| 2 | out | ICoreDouble | y |
Ports the constructor creates. A block whose port list changes with its configuration adds or removes ports at load time; the count above is the one a freshly inserted block has.
Configuration variables#
| Config variable | Default | Simulink parameter |
|---|---|---|
Weights (int8) | [96 -54 37; -28 71 -110] | — |
Weight Zero Point | 0 | — |
Weight Scale | 0.008 | — |
Bias (int32) | [420; -310] | — |
Input Scale | 0.01 | — |
Input Zero Point | 0 | — |
Output Scale | 0.02 | — |
Output Zero Point | 0 | — |
Every block also carries Sampling Time (s) from ICoreBlockSolverEnvironment: zero or less inherits the solver's rate, a positive value runs the block at that period.
Simulink bridge#
| support | Support::None |
| Simulink path | — |
| port-count rule | PortsParam::None |
SampleTime parameter | yes |
Caveat (shown to the user): no Simulink equivalent: the Deep Learning blocks take a trained network OBJECT, and the fixed-point tooling QUANTIZES a model rather than accepting an already-quantized weight set, a scale and a zero point as parameters
Catalog contract: src/ICoreSDK/ICoreCoder/ICoreCommandSystem/SimulinkBridge/ICoreSimulinkBlockCatalog.h
Description vs code#
The checker has a blind spot here — it could not resolve something (a grouped port bullet, a computed config name), which is reported and never counted as a pass. A reader has to settle it:
B0every stimulus in the sample errored — cross-checks skipped
The verdict above is
tools/docs/check_block_descriptions.py(P7.1), which compares LISTS. It cannot read a sentence: "stateless" on a block with a state, an initial-value semantic the recursion does not implement, a "not synthesizable" caveat the HDL banner contradicts. That is the agent audit (P7.3) on BLOCK_DESCRIPTION_AUDIT.md, and this tool's green is not a substitute for one.
File banner (developer view)#
The top comment of the block's .cpp — the maths, the realization and the export strategy, addressed to whoever changes it. It must not contradict the description above (P7.5).
Quantized Dense Layer — int8 weights, integer accumulation, requantized output n_j = clamp(round(u_j / Su), -128 - Zu, 127 - Zu) acc_i = B_i + SUM_j (W_ij - Zw) * n_j y_i = Sy * clamp(round(M_i * acc_i), -128 - Zy, 127 - Zy) M_i = Su * Sw_i / Sy
Read the header first: it carries the four decisions this file only implements -- the zero points collapsing into the clamp bounds, the half-away-from-zero rounding shared with Quantizer, why the three HDL targets divide the accumulator by a power of two (and why that divisor must NOT be the output multiplier), and why this is not a duplicate of Dense_Layer.
Everything a generator needs is resolved in loadBlockConfig(). Nothing below re-derives a scale or a zero point, because ten backends agreeing depends on there being one derivation.
Sample results#
No stimulus produced a sampled output in this rig — Invalid input size at: ICore Blocks/Home/Quantized Dense Layer. That is a fact about the single-block rig, not a verdict on the block: an offline batch fit, a block whose output only appears at onSolverFinish, or one that needs a driven environment cannot be exercised alone.
Category unsampled · sample time 0.1 · 60 steps · commit ccf005c8 · produced by docsSample --out <folder> --steps 60
Sample data: docs/generated/samples/Machine_Learning__Neural_Networks__Quantized_Dense_Layer.json