Algorithm Search⌘K

Wallace tree multiplier in Python

Hardware multiplier that sums partial products with a tree of carry-save adders.

File
wallace_tree_multiplier.py, 150 lines
Level
I0 not checked yet
Toolchain
Python 3.14, pytest 9
Test framework
pytest
Run the tests
python3 -m pytest
wallace_tree_multiplier.pyRawGitHub
# SPDX-License-Identifier: MIT
# Part of https://algorithm.tamnd.com/wallace-tree-multiplier/. The authors are listed in prov/N0JM8H.jsonl.

"""A Wallace tree multiplier, simulated gate by gate.

Every signal is a pair (value, depth). The partial product bits are reduced
column by column, level by level, with full adders and half adders until every
column holds at most two bits, and a Kogge-Stone prefix adder adds the two
remaining rows. A Stats object counts the gates, the adders and the levels and
records the column heights after each level.
"""


class Stats:
    def __init__(self) -> None:
        self.gates = 0
        self.full_adders = 0
        self.half_adders = 0
        self.levels = 0
        self.heights: list[list[int]] = []


def _and(x, y, st):
    st.gates += 1
    return x[0] & y[0], max(x[1], y[1]) + 1


def _or(x, y, st):
    st.gates += 1
    return x[0] | y[0], max(x[1], y[1]) + 1


def _xor(x, y, st):
    st.gates += 1
    return x[0] ^ y[0], max(x[1], y[1]) + 1


def full_adder(x, y, z, st):
    """Three bits of one column in; a sum bit for the column and a carry bit for the next."""
    st.full_adders += 1
    p = _xor(x, y, st)
    s = _xor(p, z, st)
    c = _or(_and(x, y, st), _and(p, z, st), st)
    return s, c


def half_adder(x, y, st):
    st.half_adders += 1
    return _xor(x, y, st), _and(x, y, st)


def _prefix_add(X: list, Y: list, st: Stats) -> list:
    """Kogge-Stone addition of two rows of signals of equal length; returns len + 1 signals."""
    n = len(X)
    g = [_and(X[i], Y[i], st) for i in range(n)]
    p = [_xor(X[i], Y[i], st) for i in range(n)]
    G, P = list(g), list(p)
    d = 1
    while d < n:
        nG, nP = list(G), list(P)
        for i in range(d, n):
            nG[i] = _or(G[i], _and(P[i], G[i - d], st), st)
            nP[i] = _and(P[i], P[i - d], st)
        G, P = nG, nP
        d *= 2
    zero = (0, 0)
    c = [zero] + G
    return [_xor(p[i], c[i], st) for i in range(n)] + [c[n]]


def wallace_multiply(a: list[int], b: list[int], st: Stats | None = None) -> tuple[list[int], int]:
    """Multiply two bit lists (least significant first); return the product bits and the depth."""
    if st is None:
        st = Stats()
    n, m = len(a), len(b)
    if n == 0 or m == 0:
        return [0], 0
    A = [(x & 1, 0) for x in a]
    B = [(x & 1, 0) for x in b]
    width = n + m
    cols: list[list] = [[] for _ in range(width + 1)]
    for i in range(n):  # @line L1: the n m partial product bits, a_i AND b_j in column i + j
        for j in range(m):
            cols[i + j].append(_and(A[i], B[j], st))
    st.heights.append([len(c) for c in cols[:width]])
    while max(len(c) for c in cols) > 2:  # @line L2: one level of the tree
        new: list[list] = [[] for _ in range(width + 1)]
        for k in range(width):  # @line L3: all columns in parallel
            bits = cols[k]
            t = 0
            while len(bits) - t >= 3:  # @line L4: groups of three
                s, c = full_adder(bits[t], bits[t + 1], bits[t + 2], st)  # @line L5
                new[k].append(s)
                new[k + 1].append(c)
                t += 3
            if len(bits) - t == 2:  # @line L6: a pair goes to a half adder, a single bit passes
                s, c = half_adder(bits[t], bits[t + 1], st)
                new[k].append(s)
                new[k + 1].append(c)
            elif len(bits) - t == 1:
                new[k].append(bits[t])
        cols = new
        st.levels += 1
        st.heights.append([len(c) for c in cols[:width]])
    zero = (0, 0)
    X = [cols[k][0] if len(cols[k]) > 0 else zero for k in range(width)]  # @line L7: the two rows
    Y = [cols[k][1] if len(cols[k]) > 1 else zero for k in range(width)]
    out = _prefix_add(X, Y, st)[:width]  # @line L8: one carry-propagate addition
    return [x[0] for x in out], max(x[1] for x in out)


def tree_levels(k: int) -> int:
    """The number of levels of carry-save reduction that k rows need: the least j
    with d_j >= k, where d_0 = 2 and d_{j+1} = floor(3 d_j / 2)."""
    d, j = 2, 0
    while d < k:
        d = 3 * d // 2
        j += 1
    return j


def to_bits(x: int, n: int) -> list[int]:
    return [(x >> i) & 1 for i in range(n)]


def from_bits(s: list[int]) -> int:
    return sum(bit << i for i, bit in enumerate(s))


def multiply_limbs(a: list[int], b: list[int], w: int = 8) -> list[int]:
    """Multiply two limb arrays in radix 2**w with one Wallace tree of their full width."""
    if not a or not b:
        return [0] * (len(a) + len(b))
    abits = [(a[i // w] >> (i % w)) & 1 for i in range(len(a) * w)]
    bbits = [(b[i // w] >> (i % w)) & 1 for i in range(len(b) * w)]
    s, _ = wallace_multiply(abits, bbits)
    out = [0] * (len(a) + len(b))
    for i, bit in enumerate(s):
        out[i // w] |= bit << (i % w)
    return out


def multiply_signed(x: int, y: int, n: int) -> int:
    """Two's complement n-bit operands: multiply the magnitudes and fix the sign."""
    lo, hi = -(1 << (n - 1)), (1 << (n - 1)) - 1
    if not (lo <= x <= hi and lo <= y <= hi):
        raise ValueError("operand out of range")
    s, _ = wallace_multiply(to_bits(abs(x), n), to_bits(abs(y), n))
    p = from_bits(s)
    return -p if (x < 0) != (y < 0) else p
test_wallace_tree_multiplier.py 136 lines
# SPDX-License-Identifier: MIT
# Part of https://algorithm.tamnd.com/wallace-tree-multiplier/. The authors are listed in prov/N0JM8H.jsonl.

import math
import random

import pytest

from wallace_tree_multiplier import (
    Stats,
    from_bits,
    multiply_limbs,
    multiply_signed,
    to_bits,
    tree_levels,
    wallace_multiply,
)


def limbs(x: int, w: int, n: int) -> list[int]:
    return [(x >> (w * i)) & ((1 << w) - 1) for i in range(n)]


def value(d: list[int], w: int) -> int:
    v = 0
    for x in reversed(d):
        v = (v << w) | x
    return v


def test_example() -> None:
    st = Stats()
    s, depth = wallace_multiply(to_bits(182, 8), to_bits(91, 8), st)
    assert from_bits(s) == 16562 == 182 * 91
    assert st.levels == 4
    assert [max(h) for h in st.heights] == [8, 6, 4, 3, 2]
    assert st.heights[0] == [1, 2, 3, 4, 5, 6, 7, 8, 7, 6, 5, 4, 3, 2, 1, 0]
    assert st.heights[1] == [1, 1, 2, 3, 3, 4, 5, 5, 6, 4, 4, 4, 2, 2, 2, 0]
    assert (st.full_adders, st.half_adders, st.gates, depth) == (36, 25, 489, 22)


def test_dadda_numbers() -> None:
    d = [2]
    while len(d) < 11:
        d.append(3 * d[-1] // 2)
    assert d == [2, 3, 4, 6, 9, 13, 19, 28, 42, 63, 94]
    assert [tree_levels(n) for n in (8, 16, 24, 32, 53, 64)] == [4, 6, 7, 8, 9, 10]


def test_levels_and_depth() -> None:
    fewer = []
    for n in range(1, 90):
        st = Stats()
        s, depth = wallace_multiply([1] * n, [1] * n, st)
        assert from_bits(s) == ((1 << n) - 1) ** 2
        L = st.levels
        assert L <= tree_levels(n)
        if L < tree_levels(n):
            fewer.append(n)
        assert max(st.heights[-1]) <= 2
        assert depth <= 3 * L + 2 * math.ceil(math.log2(2 * n)) + 3
        assert st.full_adders < n * n
        assert st.half_adders <= (2 * n + 1) * L
    assert fewer == [20, 29]


def test_measured_sizes() -> None:
    out = {}
    for n in (16, 32, 64):
        st = Stats()
        _, depth = wallace_multiply([1] * n, [1] * n, st)
        out[n] = (st.gates, depth)
    assert out == {16: (1875, 30), 32: (7111, 37), 64: (27111, 45)}


def test_rectangular_shapes() -> None:
    rng = random.Random(7)
    for n in range(1, 13):
        for m in range(1, 13):
            for _ in range(5):
                x, y = rng.randrange(1 << n), rng.randrange(1 << m)
                st = Stats()
                s, _ = wallace_multiply(to_bits(x, n), to_bits(y, m), st)
                assert from_bits(s) == x * y
                assert len(s) == n + m
                assert st.levels <= tree_levels(min(n, m))


def test_exhaustive_small() -> None:
    for n in (1, 2, 3, 4, 5):
        for x in range(1 << n):
            for y in range(1 << n):
                s, _ = wallace_multiply(to_bits(x, n), to_bits(y, n))
                assert from_bits(s) == x * y


@pytest.mark.parametrize("w", [4, 8])
def test_limbs_random(w: int) -> None:
    rng = random.Random(2024 + w)
    for _ in range(300):
        na, nb = rng.randint(1, 6), rng.randint(1, 6)
        x, y = rng.randrange(1 << (w * na)), rng.randrange(1 << (w * nb))
        out = multiply_limbs(limbs(x, w, na), limbs(y, w, nb), w)
        assert len(out) == na + nb
        assert value(out, w) == x * y


@pytest.mark.parametrize("w", [4, 8])
def test_limbs_edges(w: int) -> None:
    B = 1 << w
    for na in (1, 2, 5):
        for nb in (1, 3, 4):
            top_a, top_b = B**na - 1, B**nb - 1
            for x in (0, 1, top_a, B ** (na - 1), top_a - 1):
                for y in (0, 1, top_b, B ** (nb - 1), top_b // 2):
                    out = multiply_limbs(limbs(x, w, na), limbs(y, w, nb), w)
                    assert value(out, w) == x * y
    assert multiply_limbs([], [3], w) == [0]


def test_signed() -> None:
    rng = random.Random(11)
    for n in (2, 4, 8, 16):
        lo, hi = -(1 << (n - 1)), (1 << (n - 1)) - 1
        for x in (lo, -1, 0, 1, hi):
            for y in (lo, -1, 0, 1, hi):
                assert multiply_signed(x, y, n) == x * y
        for _ in range(100):
            x, y = rng.randint(lo, hi), rng.randint(lo, hi)
            assert multiply_signed(x, y, n) == x * y
    with pytest.raises(ValueError):
        multiply_signed(8, 1, 4)


def test_empty() -> None:
    assert wallace_multiply([], [1]) == ([0], 0)

What the next level needs

  • the tests did not run in this build

Read the full page about Wallace tree multiplier