gemmlowp

repository·master·Indexed 23 days ago

https://github.com/google/gemmlowp

A small, self-contained library for low-precision General Matrix Multiplication (GEMM) optimized for speed and low power consumption in mobile and embedded environments. It features a headers-only public interface, support for x86 (SSE 4.1) and NEON architectures, and a three-stage computation scheme (Pack, Compute, Unpack) to balance memory efficiency and arithmetic performance. The library also supports less-than-8-bit computation via BitDepthParams and BitDepthSetting to trade numerical accuracy for higher throughput.

Tokens
6.9K
Snippets
10
Records
40
Agent score
83%

What's inside gemmlowp

  1. How gemmlowp performs low-precision GEMM

    master

    gemmlowp implements low-precision General Matrix Multiplication (GEMM) using a three-stage computation scheme to balance memory efficiency and arithmetic performance.

    Because the library uses uint8 inputs/outputs but accumulates in int32 to maintain precision, it must manage the transition between these types. The process follows these steps:

    1. Pack: Reorder input matrix blocks (LHS/RHS) into a layout optimized for cache locality and SIMD loading.
    2. Compute: Execute the multiplication using a kernel that operates on the packed blocks and accumulates results into a temporary int32 block.
    3. Unpack: Convert the int32 accumulator block back into the uint8 destination matrix.

    This approach minimizes the memory footprint of high-precision int32 values by only storing and processing them in small, manageable blocks.

    1. Pack lhs/rhs blocks from the input matrices.
    2. Compute the product of the packed blocks, using the kernel.
    3. Unpack the result block into the output matrix.
  2. How gemmlowp handles offsets efficiently

    master

    To maintain high performance and low memory bandwidth, gemmlowp does not add lhs_offset and rhs_offset to every entry during the GEMM kernel execution. Instead, it uses a mathematical expansion of the matrix product:

    $$(lhs + lhs_offset \cdot P) \cdot (rhs + rhs_offset \cdot Q) =$

    1. lhs * rhs (The core GEMM kernel only computes this term)
    2. lhs_offset * P * rhs (A rank-one update: lhs_offset times the column sums of rhs)
    3. lhs * rhs_offset * Q (A rank-one update: rhs_offset times the row sums of lhs)
    4. lhs_offset * rhs_offset * depth (A rank-zero update: a constant added to all entries, where depth is the number of columns in lhs)

    Implementation Details:

    • Packing Stage: gemmlowp computes the sum of each row of the lhs block and the sum of each column of the rhs block. These are stored in a buffer managed by sums_of_each_slice_handle_ in the PackedSideBlock class.
    • Compute Kernel Stage: The kernel only computes the first term (lhs * rhs) to maximize speed and register availability.
    • Unpacking Stage: The rank-one and rank-zero updates are applied during the unpacking stage (e.g., in UnpackResultImpl) to finalize the result.
  3. Understand the gemmlowp quantization paradigm

    master

    A quantization paradigm in gemmlowp defines the correspondence between matrices of quantized 8-bit values (typically uint8) and matrices of real numbers. This choice determines how the library transforms internal 32-bit accumulators into final 8-bit outputs.

    To implement a custom arithmetic paradigm, use the GemmWithOutputPipeline entry point, which allows you to specify an arbitrary output pipeline.

  4. How less-than-8-bit computation works (Packing, Kernel, Unpacking)

    master

    Less-than-8-bit computation achieves higher throughput by reducing the range of operands, allowing for narrower accumulators (e.g., uint16) without overflow risk. The process follows three stages:

    1. Packing Stage (Requantization)

    Input matrices (range [0...255]) are scaled down to the range specified by the bit depth: [0 ... (2^N)-1]. For example, a 5-bit depth scales values to [0...31]. This "requantization" is computationally expensive and can make packing slower, meaning this mode is most effective for large, square matrices where packing overhead is negligible compared to the $O(N^3)$ compute cost.

    2. Computation Kernel Stage

    Because inputs are restricted to a smaller range, products are also smaller. If LHS entries are $2^N$ and RHS entries are $2^M$, each product is $\le 2^{M+N}$. This allows accumulating $2^{16-(M+N)}$ products into a uint16 accumulator before risking overflow. On SIMD architectures, using uint16 accumulators can potentially double arithmetic throughput compared to uint32 accumulators.

    3. Unpacking Stage

    To compensate for the scaling applied during packing, the result values are scaled back up. The output is scaled by the following factor:

    $$\frac{255 \times 255}{(2^{lhs_bits} - 1) \times (2^{rhs_bits} - 1)}$$

    This is typically implemented using a MultiplyByConstantFraction function.

  5. Use the flexible output pipeline paradigm

    master

    The current recommended way to perform low-precision matrix multiplication in gemmlowp is using an "output pipeline." This design allows you to define a chain of transformations to be applied to internal 32-bit accumulators to produce the final 8-bit outputs. This approach is more flexible and avoids the overflow risks associated with legacy integer multiplication.

    To use this paradigm, you provide:

    1. A lhs matrix of uint8_t quantized values.
    2. A rhs matrix of uint8_t quantized values.
    3. An int32 lhs_offset to be added to each entry of the lhs matrix.
    4. An int32 rhs_offset to be added to each entry of the rhs matrix.
    5. An output pipeline to process the resulting int32 accumulators.

    The computation follows these steps:

    1. Cast lhs entries to int32 and add lhs_offset.
    2. Cast rhs entries to int32 and add rhs_offset.
    3. Compute the int32 matrix product.
    4. Apply the output pipeline to the int32 accumulators to obtain final outputs.
  6. Understand Requantization in the Packing Stage

    master

    During the packing stage of less-than-8-bit computation, Requantize() is used to map input matrix data from the standard 8-bit range [0 ... 255] to a smaller bit-depth range [0 ... (2^N)-1].

    Choosing the correct rounding method is critical for accuracy:

    1. Rounding-to-nearest: Uses the formula dst = (src * maxval + rounding_offset) / 255, where rounding_offset = 127. While theoretically unbiased, it can introduce significant bias in practice when input data is non-uniformly distributed or belongs to a small finite set. This bias causes errors that grow linearly with the GEMM depth.
    2. Probabilistic rounding: Uses the same formula but with a random rounding_offset in the range [0 .. 254]. This guarantees zero bias regardless of the input distribution. However, it has a higher error variance (2x higher), meaning the error term grows with the square root of the GEMM depth.

    Recommendation: Use probabilistic rounding for large GEMM depths and rounding-to-nearest for smaller GEMM depths.

  7. Understand the gemmlowp computation pipeline

    master

    The gemmlowp architecture is organized into a hierarchical pipeline designed to maximize cache hits and register usage. A typical execution flow follows this nested structure:

    1. L2 Block Level: Subdivide matrices into blocks that fit in the L2 cache.
    2. Packing: Pack the L2 blocks to ensure efficient traversal.
    3. L1/Sub-block Level: Subdivide L2 blocks into smaller sub-blocks that fit in the L1 cache.
    4. Kernel Execution: The inner-most loop (the GEMM kernel) performs the actual multiply-accumulate operations using architecture-specific instructions (often written in assembly or SIMD) to maximize register usage.

    By organizing data this way, the library minimizes redundant memory accesses, which are the primary bottleneck in matrix multiplication.

    allocate(some_lhs_L2_block);
    allocate(some_rhs_L2_block);
    // new: temp storage for int32 accums
    allocate(some_int32_accumulators_block);
    for (some_lhs_L2_block) {
      pack(some_lhs_L2_block);
      for (some_rhs_L2_block) {
        pack(some_rhs_L2_block);
        for (some_lhs_sub_block in some_lhs_L2_block) {
          for (some_rhs_sub_block in some_rhs_L2_block) {
            // new: pass int32 accums to kernel
            kernel(&some_int32_accumulators_block,
                   some_lhs_sub_block,
                   some_rhs_sub_block);
          }
        }
        // new: unpack int32 accums into destination matrix
        unpack(some_int32_accumulators_block);
      }
    }
  8. Use the gemmlowp public interface

    master

    The main public interface is located in the public/ subdirectory.

    Key characteristics:

    • Headers-only: There is nothing to link against; simply include the headers in your project.
    • Usage: For a complete example of quantizing float matrices and performing quantized matrix multiplication, refer to doc/quantization_example.cc.
    • Legacy Note: The eight_bit_int_gemm/ interface is deprecated and should not be used for new projects.
  9. Optimize GEMM performance with recommended storage orders

    master

    While gemmlowp supports arbitrary combinations of storage orders for the LHS, RHS, and result matrices, it is highly optimized for neural network inference workloads using a specific configuration.

    To achieve optimal performance, use the following storage orders:

    • LhsOrder: RowMajor (typically used for constant weights).
    • RhsOrder: ColMajor (typically used for input activations).
    • ResultOrder: ColMajor (typically used for output activations).

    Using this configuration (RowMajor, ColMajor, ColMajor) ensures that the RHS and result share the same storage order, allowing the output of one layer to be used directly as the input for the next. Using other combinations will result in less efficient paths during the packing and unpacking stages, though the compute kernel stage remains unaffected.

  10. Understand the packing stage in gemmlowp

    master

    The packing stage is the first of three stages in the gemmlowp computation pipeline (packing, kernel, unpacking). Its primary purpose is to reorder matrix data (Lhs/Rhs) into a storage format that matches the traversal pattern used by the compute kernel. This ensures high cache efficiency (L1/L2) and register-level performance.

    Key responsibilities of the packing stage include:

    1. Reordering data: Organizing blocks into a 'Z-order' or 'fractal order' to optimize cache hits.
    2. Computing sum vectors: Calculating vectors of sums along the depth dimension (used during the unpacking stage).
    3. Requantization: If the BitDepthSetting requires less than 8 bits of precision, the packing stage performs requantization via the Requantize() function.
  11. Avoid the legacy EightBitIntGemm interface

    master

    The EightBitIntGemm paradigm (exposed via the Gemm entry point in public/gemmlowp.h and the eight_bit_int_gemm directory) is considered deprecated and is not recommended for new usage.

    In this legacy paradigm, the output is calculated by adding a result_offset and then multiplying by a fraction defined as:

    $$\frac{\text{result_mult_int}}{2^{\text{result_shift}}}$$

    This method is discouraged because the integer multiplication by the numerator risks overflowing. The modern "output pipeline" approach avoids this by using fixed-point multiplication instead of ordinary integer multiplication.

  12. How GEMM kernels work in gemmlowp

    master

    In gemmlowp, a GEMM kernel is an implementation of the innermost loop of a General Matrix Multiplication (GEMM). To achieve high performance, the kernel must operate over the 'depth' dimension so that it can accumulate results into a small number of registers.

    Each kernel consists of two primary components:

    1. A Format: A typedef that dictates the specific data layout the kernel expects. This allows kernels to maximize the ratio of arithmetic instructions to memory access by handling blocks as wide as the CPU registers allow.
    2. A Run method: The actual implementation of the computation logic.

    By making kernels swappable and allowing them to define their own data formats, gemmlowp enables efficient specialization for different CPU architectures.