UH-OH

It looks like you don’t have access to that feature yet

Contact sales to get upgraded to the full DevStudio experience.

UH-OH

It looks like you don't have access to that feature yet.

to select
to navigate
escto close
Chimera Compute Library (CCL) API ReferencePython API Guide for Quadric SDK

Python API Guide for Quadric SDK

Python API Guide for Quadric SDK

The Quadric SDK provides a comprehensive Python API (quadric package) that allows you to interact with EPU (Embedded Processing Unit) hardware through an intuitive Python interface. This guide covers all features from device creation to kernel execution with real-world examples.

Table of Contents

  1. Quick Start
  2. Installation & Import
  3. Device Management
  4. Device Configuration
  5. Memory Management
  6. Kernel Operations
  7. Data Transfer
  8. Complete Examples
  9. Advanced Features
  10. API Reference

Quick Start

Here's a minimal example to get you started:

import quadric
import numpy as np

## Create and configure device
device_manager = quadric.DeviceManager()
config = quadric.DeviceConfiguration()
device = device_manager.get_device(config)

## Load and run kernel (path without .qo extension)
kernel = quadric.get_kernel("path/to/my_kernel")  # Omit .qo extension
device.load_kernel(kernel)

## Prepare data
input_data = np.random.randn(1, 64, 224, 224).astype(np.float32)
input_buffer = device.allocate_and_copy_ndarray(input_data)
output_buffer = device.allocate(list(input_data.shape), np.dtype(input_data.dtype))

## Execute kernel
device.run_kernel("process", [input_buffer, output_buffer])

## Get results
output = device.copy_ndarray_from_device(output_buffer, list(input_data.shape), np.dtype(input_data.dtype))

## Cleanup
device.free(input_buffer)
device.free(output_buffer)

Installation & Import

The quadric package is installed as part of the Quadric SDK. Import it in your Python scripts:

import quadric
import numpy as np  # NumPy is required for array operations

The package exports the following main components:

from quadric import (
    DeviceManager,          # Manages device creation
    Device,                 # Device instance
    DeviceConfiguration,    # Device configuration
    Kernel,                 # Kernel object
    KernelArgument,         # Kernel argument wrapper
    DeviceBufferRef,        # Device buffer reference
    DeviceBufferOffsetRef,  # Buffer offset reference
    ReservedKernelParams,   # Reserved kernel parameter enums
    get_kernel,             # Load kernel from file
)

## Additional enums available in host_python_bindings submodule
from quadric.host_python_bindings import (
    MacCfg,                 # MAC configuration: M8, M16, M32
    WeightModeEnabled,      # Weight mode: Enabled, Disabled
)

Device Management

Creating a Device Manager

The DeviceManager is your entry point for all device operations:

import quadric

## Create device manager
device_manager = quadric.DeviceManager()

Single-Core Device

Create a device with a single processing cluster:

import quadric

## Create device manager
device_manager = quadric.DeviceManager()

## Configure device
config = quadric.DeviceConfiguration()
config.profile = True
config.print_profile_cycles = True

## Create device
device = device_manager.get_device(config)

## Check device type
print(f"Running on: {device.get_device_type_string()}")

Multi-Core Device

Create a device with multiple cores for parallel processing:

import quadric

device_manager = quadric.DeviceManager()

## Create configurations for each core
configs = [
    quadric.DeviceConfiguration(),
    quadric.DeviceConfiguration(),
    quadric.DeviceConfiguration(),
    quadric.DeviceConfiguration(),
]

## Create multi-core device
device = device_manager.get_multicore_device(configs)

Multi-Cluster Device

Create a device with multiple clusters of cores:

import quadric

device_manager = quadric.DeviceManager()

## Configure multi-cluster device
cluster_size = 4      # Cores per cluster
num_clusters = 2      # Number of clusters
config = quadric.DeviceConfiguration()

## Create multi-cluster device
device = device_manager.get_multicluster_device(
    cluster_size,
    num_clusters,
    config
)

Device Configuration

The DeviceConfiguration class provides extensive control over device parameters. Understanding these settings is crucial for optimal performance.

Basic Configuration

import quadric

## Create configuration
config = quadric.DeviceConfiguration()

Standard Product Configurations

Quadric offers three standard product configurations:

from quadric.host_python_bindings import MacCfg, WeightModeEnabled
import quadric

## QC-N: 8x8 core configuration (64 cores)
def create_qc_n_config():
    config = quadric.DeviceConfiguration()
    config.core_dim = 8
    config.border_width = 2
    config.ocm_size = 16 << 20  # 16 MB
    config.mac_cfg = MacCfg.M16  # 16 MACs per PE
    return config

## QC-P: 16x16 core configuration (256 cores)
def create_qc_p_config():
    config = quadric.DeviceConfiguration()
    config.core_dim = 16
    config.border_width = 2
    config.ocm_size = 16 << 20  # 16 MB
    config.mac_cfg = MacCfg.M16  # 16 MACs per PE
    return config

## QC-U: 32x32 core configuration (1024 cores)
def create_qc_u_config():
    config = quadric.DeviceConfiguration()
    config.core_dim = 32
    config.border_width = 2
    config.ocm_size = 32 << 20  # 32 MB
    config.mac_cfg = MacCfg.M16  # 16 MACs per PE
    return config

## Usage
device_manager = quadric.DeviceManager()
config = create_qc_u_config()  # Choose your product configuration
device = device_manager.get_device(config)

Hardware Architecture

Configure the physical hardware parameters:

from quadric.host_python_bindings import MacCfg, WeightModeEnabled

config = quadric.DeviceConfiguration()

## Core array dimensions
config.core_dim = 8          # Number of cores in array: 8, 16, or 32
config.border_width = 2      # Border cores: 1 or 2

## MAC (Multiply-Accumulate) configuration
config.mac_cfg = MacCfg.M16  # Options: M8, M16, M32 (MACs per PE)

## Memory sizes
config.ocm_size = 16 * 1024 * 1024    # On-Chip Memory size (16 MB)
config.inst_mem_depth = 65536          # Instruction memory depth (bytes)

## Timing
config.cycle_time_ns = 1.0             # Cycle time in nanoseconds

## 4-bit weight mode for neural networks
config.four_bit_mode_en = WeightModeEnabled.Disabled

DDR Memory Configuration

DDR is the external memory for large data storage:

## DDR Read Configuration
config.ddr_rd_bw_total = 192000      # Total read bandwidth (Mbps)
config.ddr_rd_avg_pct = 80           # Average utilization (0-100)
config.ddr_rd_max_pct = 100          # Maximum utilization (0-100)
config.ddr_rd_burst_limit = 256      # Burst size limit (bytes)
config.ddr_avg_rd_latency = 100      # Average read latency (cycles)

## DDR Write Configuration
config.ddr_wr_bw_total = 192000      # Total write bandwidth (Mbps)
config.ddr_wr_avg_pct = 80           # Average utilization (0-100)
config.ddr_wr_max_pct = 100          # Maximum utilization (0-100)
config.ddr_wr_burst_limit = 256      # Burst size limit (bytes)
config.ddr_avg_wr_latency = 100      # Average write latency (cycles)

## DDR Bus Configuration
config.ddr_axi_width = 512           # AXI bus width (bits)

OCM Configuration

OCM (On-Chip Memory) is fast intermediate storage:

## OCM Read Configuration
config.ocm_rd_bw_total = 4096000     # Total read bandwidth (Mbps)
config.ocm_rd_avg_pct = 90           # Average utilization (0-100)
config.ocm_rd_max_pct = 100          # Maximum utilization (0-100)
config.ocm_rd_burst_limit = 1024     # Burst size limit (bytes)

## OCM Write Configuration
config.ocm_wr_bw_total = 4096000     # Total write bandwidth (Mbps)
config.ocm_wr_avg_pct = 90           # Average utilization (0-100)
config.ocm_wr_max_pct = 100          # Maximum utilization (0-100)
config.ocm_wr_burst_limit = 1024     # Burst size limit (bytes)

IFU Configuration

IFU (Instruction Fetch Unit) handles instruction memory:

## IFU Bandwidth Configuration
config.ifu_rd_bw_total = 512000      # Total read bandwidth (Mbps)
config.ifu_rd_avg_pct = 85           # Average utilization (0-100)
config.ifu_rd_max_pct = 100          # Maximum utilization (0-100)
config.ifu_rd_burst_limit = 512      # Burst size limit (bytes)
config.ifu_avg_rd_latency = 50       # Average read latency (cycles)

## IFU Cache Configuration
config.ifu_cache_line_width_bytes = 64    # Cache line width
config.ifu_cache_total_bytes = 32768      # Total cache size

Profiling & Debug Configuration

Enable profiling to analyze kernel performance:

config = quadric.DeviceConfiguration()

## Enable profiling
config.profile = True                     # Enable performance profiling
config.print_profile_cycles = True        # Print cycle counts

## Bandwidth simulation
config.inf_bw = False                     # Set True for infinite bandwidth (debug)

## Memory initialization
config.random_init = False                # Random memory initialization
config.seed = 42                          # Random seed (when random_init=True)

## Memory tracing
config.mem_trace_enable = False           # Enable memory access tracing
config.mem_trace_filename = "trace.txt"   # Trace output file

## Instruction execution counts
config.inst_exec_counts_filename = "inst_counts.txt"

## Event streaming (for debug)
config.event_stream_target = "NONE"       # Options: NONE, STDERR, STDOUT, <filename>

## Power profiling
config.power_profile_target = "NONE"      # Options: NONE, STDERR, STDOUT, <filename>
config.power_profile_map_file = ""        # JSON event mappings
config.power_profile_report_period = 0    # Report every N cycles (0=disabled)

## Cycle Heartbeat
config.cycle_count_heartbeat_target = "NONE" # Options: NONE, STDERR, STDOUT, <filename>
config.cycle_count_heartbeat_period = 1000.     # Report every N cycles

## Kernel flow configuration
config.post_kernel_flow_timeout_cycles = 10000  # Timeout after kernel completes

## ECC modeling
config.enable_ecc_cycle_modeling = False  # Model ECC performance impact

Memory Management

Buffer References

Quadric uses two types of buffer references:

  • DeviceBufferRef: Reference to an allocated device buffer
  • DeviceBufferOffsetRef: Reference to a specific offset within a buffer
import quadric
import numpy as np

## Create buffer reference
buffer = quadric.DeviceBufferRef()

## Access buffer attributes
print(f"Buffer ID: {buffer.buffer_id}")

## Create offset reference (512 bytes into buffer)
offset_buffer = buffer[512]
print(f"Offset: {offset_buffer.offset}")

Allocating Device Memory

Allocate memory on the device:

import numpy as np

## Method 1: Allocate empty buffer
shape = [1, 64, 224, 224]  # [batch, channel, height, width]
dtype = np.dtype(np.float32)  # Must use np.dtype() constructor
buffer = device.allocate(shape, dtype)

## Method 2: Allocate and copy data in one step (recommended)
input_data = np.random.randn(1, 64, 224, 224).astype(np.float32)
buffer = device.allocate_and_copy_ndarray(input_data)

## Allocate multiple buffers
input_a = device.allocate_and_copy_ndarray(matrix_a)
input_b = device.allocate_and_copy_ndarray(matrix_b)
output = device.allocate(output_shape, np.dtype(np.int8))

Freeing Device Memory

Always free buffers when done to prevent memory leaks:

## Free single buffer
device.free(buffer)

## Free multiple buffers
device.free(input_buffer)
device.free(output_buffer)

Using Buffer Offsets

Access specific locations within a buffer:

## Allocate large buffer
large_buffer = device.allocate([1, 128, 512, 512], np.dtype(np.float32))

## Create buffer offset references
offset_ref_1 = quadric.DeviceBufferOffsetRef()
offset_ref_1.buffer_id = large_buffer.buffer_id
offset_ref_1.offset = 0       # Start of buffer

offset_ref_2 = quadric.DeviceBufferOffsetRef()
offset_ref_2.buffer_id = large_buffer.buffer_id
offset_ref_2.offset = 1024    # 1024 bytes in

## Copy to specific offsets
device.copy_ndarray_to_device(data1, offset_ref_1)
device.copy_ndarray_to_device(data2, offset_ref_2)

Kernel Operations

Loading Kernels

Load compiled kernels from binary files:

import quadric

## Load kernel from file (path WITHOUT .qo extension - it will be added automatically)
kernel = quadric.get_kernel("path/to/kernel")  # Not "kernel.qo"

## Load kernel to device at default offset (0)
device.load_kernel(kernel)

## Load kernel at specific instruction memory offset
device.load_kernel(kernel, offset=1024)

Running Kernels

Execute kernels with or without arguments:

Running with Arguments

## Prepare kernel arguments
alpha = 2
beta = 1

## Run kernel with scalar and buffer arguments
device.run_kernel("gemm_entrypoint", [
    alpha,          # Scalar argument (int32)
    buffer_a,       # Buffer reference
    buffer_b,       # Buffer reference
    beta,           # Scalar argument (int32)
    buffer_c,       # Buffer reference
    buffer_out      # Output buffer reference
])

Running without Arguments

## Run kernel with no arguments
device.run_kernel("simple_kernel")

Running on Specific Cores

For multi-core devices, specify the core_id parameter to execute a kernel on a specific core:

## Create multi-core device
num_cores = 2
configs = [quadric.DeviceConfiguration() for _ in range(num_cores)]
device_manager = quadric.DeviceManager()
device = device_manager.get_multicore_device(configs)

## Load kernel to all cores
kernel = quadric.get_kernel("multi_kernel")
device.load_kernel(kernel)

## Prepare data
input_data = np.full([1, 64, 224, 224], 100, dtype=np.int32)
input_buffer = device.allocate_and_copy_ndarray(input_data)
output_buffer_0 = device.allocate([1, 64, 224, 224], np.dtype(np.int32))
output_buffer_1 = device.allocate([1, 64, 224, 224], np.dtype(np.int32))

## Run different kernels on different cores (arguments first, then core_id)
device.run_kernel("add_one", [input_buffer, output_buffer_0], 0)  # Core 0
device.run_kernel("add_two", [input_buffer, output_buffer_1], 1)  # Core 1

## Get results from each core
output_0 = device.copy_ndarray_from_device(output_buffer_0, [1, 64, 224, 224], np.dtype(np.int32))
output_1 = device.copy_ndarray_from_device(output_buffer_1, [1, 64, 224, 224], np.dtype(np.int32))

print(f"Core 0 result: {output_0.flat[0]}")  # 101
print(f"Core 1 result: {output_1.flat[0]}")  # 102

Kernel Arguments

Supported argument types:

import numpy as np

## Scalar arguments (automatically converted)
int8_arg = np.int8(10)
uint8_arg = np.uint8(255)
int16_arg = np.int16(1000)
uint16_arg = np.uint16(65000)
int32_arg = 42                    # Python int becomes int32
uint32_arg = np.uint32(4200000000)

## Buffer arguments
buffer_arg = device.allocate_and_copy_ndarray(data)
offset_arg = buffer_arg[512]

## Pass arguments to kernel
device.run_kernel("my_kernel", [
    int32_arg,      # Scalar
    buffer_arg,     # Full buffer
    offset_arg      # Buffer at offset
])

Data Transfer

Host to Device

Transfer NumPy arrays from host to device:

import numpy as np

## Create NumPy array
input_data = np.random.randint(-128, 127, size=(1, 64, 224, 224), dtype=np.int8)

## Method 1: Allocate and copy separately
buffer = device.allocate(input_data.shape, input_data.dtype)
device.copy_ndarray_to_device(input_data, buffer)

## Method 2: Allocate and copy in one step (recommended)
buffer = device.allocate_and_copy_ndarray(input_data)

## Copy to specific buffer offset
offset_ref = quadric.DeviceBufferOffsetRef()
offset_ref.buffer_id = buffer.buffer_id
offset_ref.offset = 1024
device.copy_ndarray_to_device(data, offset_ref)

Device to Host

Transfer data from device back to host:

## Copy data from device to host
output_shape = [1, 64, 224, 224]
output_dtype = np.dtype('float32')

output_data = device.copy_ndarray_from_device(
    buffer,
    output_shape,
    output_dtype
)

## output_data is now a NumPy array on the host
print(f"Output shape: {output_data.shape}")
print(f"Output dtype: {output_data.dtype}")

Supported Data Types

The quadric package supports integer data types for computation. The EPU hardware uses fixed-point arithmetic, not floating-point.

import numpy as np

## Supported integer types (used for fixed-point representation)
np.int8     # 8-bit signed integer
np.uint8    # 8-bit unsigned integer
np.int16    # 16-bit signed integer
np.uint16   # 16-bit unsigned integer
np.int32    # 32-bit signed integer
np.uint32   # 32-bit unsigned integer

## Example usage with fixed-point data
data_int8 = np.random.randint(-128, 127, size=(1, 64, 224, 224), dtype=np.int8)
data_int32 = np.random.randint(-1000, 1000, size=(1, 64, 224, 224), dtype=np.int32)

buffer_int8 = device.allocate_and_copy_ndarray(data_int8)
buffer_int32 = device.allocate_and_copy_ndarray(data_int32)

Fixed-Point Conversion

If you have floating-point data, convert it to fixed-point before transferring to the device:

import numpy as np

def float_to_fixed(float_input, frac_bits, int_type=np.int32):
    """
    Convert floating-point to fixed-point representation.

    Args:
        float_input: Input array with floating-point values
        frac_bits: Number of fractional bits
        int_type: Integer type for fixed-point (e.g., np.int32, np.int16)

    Returns:
        Fixed-point representation as integer array

    Example:
        # Convert float with 16 fractional bits to int32
        float_data = np.array([1.5, -2.25, 3.125])
        fixed_data = float_to_fixed(float_data, frac_bits=16, int_type=np.int32)
        # Result: [98304, -147456, 204800] representing [1.5, -2.25, 3.125]
    """
    return (float_input * 2**frac_bits).astype(int_type)

def fixed_to_float(fixed_input, frac_bits, float_type=np.float32):
    """
    Convert fixed-point back to floating-point.

    Args:
        fixed_input: Input array with fixed-point values
        frac_bits: Number of fractional bits
        float_type: Float type for output (e.g., np.float32)

    Returns:
        Floating-point representation
    """
    return (fixed_input.astype(float_type) / 2**frac_bits).astype(float_type)

## Usage example
float_data = np.array([1.5, -2.25, 3.125], dtype=np.float32)

## Convert to fixed-point (16 fractional bits)
fixed_data = float_to_fixed(float_data, frac_bits=16, int_type=np.int32)

## Transfer to device
buffer = device.allocate_and_copy_ndarray(fixed_data)

## ... run kernel ...

## Convert back to float after reading from device
output_fixed = device.copy_ndarray_from_device(buffer, [3], np.dtype(np.int32))
output_float = fixed_to_float(output_fixed, frac_bits=16, float_type=np.float32)

Non-Contiguous Arrays

The quadric package automatically handles non-contiguous arrays:

import numpy as np

## Create non-contiguous array (e.g., via slicing)
original = np.random.randn(1, 128, 224, 224).astype(np.float32)
sliced = original[:, ::2, :, :]  # Every 2nd channel (non-contiguous)

print(f"Is contiguous: {sliced.flags['C_CONTIGUOUS']}")  # False

## Quadric automatically handles conversion
buffer = device.allocate_and_copy_ndarray(sliced)  # Works correctly!

## But for performance, you can manually ensure contiguity
contiguous = np.ascontiguousarray(sliced)
buffer = device.allocate_and_copy_ndarray(contiguous)

Complete Examples

Example 1: Matrix Multiplication (GEMM)

A complete GEMM example demonstrating the full workflow:

import quadric
import numpy as np
import sys

def gemm_example():
    """
    Matrix multiplication: Output = alpha * A @ B + beta * C
    where A is [M, K], B is [K, N], C is [M, N]
    """
    # Step 1: Create and configure device
    device_manager = quadric.DeviceManager()
    config = quadric.DeviceConfiguration()

    # Enable profiling to see performance
    config.profile = True
    config.print_profile_cycles = True

    device = device_manager.get_device(config)

    # Step 2: Load kernel (omit .qo extension)
    kernel = quadric.get_kernel("gemm_kernel")
    device.load_kernel(kernel)

    # Step 3: Prepare input data
    M, N, K = 128, 128, 64

    matrix_a = np.random.randint(0, 2, size=(1, 1, M, K), dtype=np.int8)
    matrix_b = np.random.randint(0, 2, size=(1, 1, K, N), dtype=np.int8)
    matrix_c = np.random.randint(0, 2, size=(1, 1, M, N), dtype=np.int8)

    alpha = 2
    beta = 1

    # Step 4: Compute expected output (for verification)
    expected = alpha * np.matmul(matrix_a[0][0], matrix_b[0][0]) + beta * matrix_c
    expected = expected.astype(np.int8)

    # Step 5: Allocate device memory and copy data
    buffer_a = device.allocate_and_copy_ndarray(matrix_a)
    buffer_b = device.allocate_and_copy_ndarray(matrix_b)
    buffer_c = device.allocate_and_copy_ndarray(matrix_c)
    buffer_out = device.allocate([1, 1, M, N], np.dtype(np.int8))

    # Step 6: Execute kernel
    device.run_kernel("gemm_entrypoint", [
        alpha,
        buffer_a,
        buffer_b,
        beta,
        buffer_c,
        buffer_out
    ])

    # Step 7: Print profiling information
    device.print_profile()

    # Step 8: Copy results back to host
    output = device.copy_ndarray_from_device(
        buffer_out,
        [1, 1, M, N],
        np.dtype(np.int8)
    )

    # Step 9: Verify results
    np.testing.assert_allclose(
        output,
        expected,
        err_msg="Output doesn't match expected result"
    )

    print("✓ GEMM test passed!")

    # Step 10: Clean up device memory
    device.free(buffer_a)
    device.free(buffer_b)
    device.free(buffer_c)
    device.free(buffer_out)

    return output

if __name__ == "__main__":
    result = gemm_example()
    print(f"Result shape: {result.shape}")

Example 2: Convolution Operation

Image processing with convolution kernels:

import quadric
import numpy as np

def convolution_example():
    """
    2D Convolution operation on image data
    """
    # Device setup
    device_manager = quadric.DeviceManager()
    config = quadric.DeviceConfiguration()
    device = device_manager.get_device(config)

    # Load convolution kernel (omit .qo extension)
    kernel = quadric.get_kernel("conv_kernel")
    device.load_kernel(kernel)

    # Prepare input data
    batch_size = 1
    in_channels = 64
    out_channels = 128
    height = 56
    width = 56
    filter_size = 3

    # Input tensor: [batch, channels, height, width]
    input_shape = (batch_size, in_channels, height, width)
    input_tensor = np.random.randint(-128, 127, size=input_shape, dtype=np.int8)

    # Weight tensor for convolution filters
    weight_shape = (1, 1, 1, out_channels * in_channels * filter_size * filter_size)
    weights = np.random.randint(-128, 127, size=weight_shape, dtype=np.int8)

    # Output shape
    output_shape = (batch_size, out_channels, height, width)

    # Allocate device memory
    input_buffer = device.allocate_and_copy_ndarray(input_tensor)
    weight_buffer = device.allocate_and_copy_ndarray(weights)
    output_buffer = device.allocate(list(output_shape), np.dtype(np.int8))

    # Run convolution kernel
    device.run_kernel("conv_entrypoint", [
        input_buffer,
        weight_buffer,
        output_buffer
    ])

    # Get results
    output = device.copy_ndarray_from_device(
        output_buffer,
        list(output_shape),
        np.dtype(np.int8)
    )

    print(f"✓ Convolution completed")
    print(f"  Input shape:  {input_tensor.shape}")
    print(f"  Output shape: {output.shape}")

    # Cleanup
    device.free(input_buffer)
    device.free(weight_buffer)
    device.free(output_buffer)

    return output

if __name__ == "__main__":
    result = convolution_example()

Example 3: Fully Connected Layer with Quantization

Neural network fully connected layer with quantized operations:

import quadric
import numpy as np

def fully_connected_example():
    """
    Fully connected (dense) layer: Output = Input @ Weights
    With quantization support
    """
    # Device setup
    device_manager = quadric.DeviceManager()
    config = quadric.DeviceConfiguration()
    config.profile = True
    config.print_profile_cycles = True

    device = device_manager.get_device(config)

    # Load kernel (omit .qo extension)
    kernel = quadric.get_kernel("fc_kernel")
    device.load_kernel(kernel)

    # Prepare input data (e.g., from previous layer)
    batch_size = 1
    input_features = 1024
    output_features = 2048

    # Quantized input (int8)
    input_tensor = np.random.randint(
        -128, 127,
        size=(batch_size, input_features),
        dtype=np.int8
    )

    # Quantized weights (int8)
    weight_tensor = np.random.randint(
        -128, 127,
        size=(output_features, input_features),
        dtype=np.int8
    )

    # Allocate and copy to device
    input_buffer = device.allocate_and_copy_ndarray(input_tensor)
    weight_buffer = device.allocate_and_copy_ndarray(weight_tensor)
    output_buffer = device.allocate(
        [batch_size, output_features],
        np.dtype(np.int8)
    )

    # Run fully connected kernel
    device.run_kernel("fc_entrypoint", [
        input_buffer,
        weight_buffer,
        output_buffer
    ])

    # Print profiling info
    device.print_profile()

    # Get results
    output = device.copy_ndarray_from_device(
        output_buffer,
        [batch_size, output_features],
        np.dtype(np.int8)
    )

    print(f"✓ Fully connected layer completed")
    print(f"  Input:  {input_tensor.shape}")
    print(f"  Weight: {weight_tensor.shape}")
    print(f"  Output: {output.shape}")

    # Cleanup
    device.free(input_buffer)
    device.free(weight_buffer)
    device.free(output_buffer)

    return output

if __name__ == "__main__":
    result = fully_connected_example()

Advanced Features

Performance Profiling

Profile kernel execution for performance analysis:

import quadric

## Enable profiling in configuration
config = quadric.DeviceConfiguration(num_cores=8)
config.profile = True
config.print_profile_cycles = True

device = device_manager.get_device(config)

## Load and run kernel
kernel = quadric.get_kernel("kernel.bin")
device.load_kernel(kernel)
device.run_kernel("entrypoint", [input_buffer, output_buffer])

## Print profiling information for all cores
device.print_profile()

## Print profiling for specific core
device.print_profile(core_id=0)

The profiling output includes:

  • Cycle counts
  • Memory bandwidth utilization
  • Cache statistics
  • Instruction execution counts

Interrupt Handling

Set up interrupt handlers for kernel synchronization:

import quadric

def interrupt_example():
    """
    Use interrupts for host-device synchronization
    """
    # Setup
    device_manager = quadric.DeviceManager()
    config = quadric.DeviceConfiguration(num_cores=8)
    device = device_manager.get_device(config)

    kernel = quadric.get_kernel("interrupt_kernel.bin")
    device.load_kernel(kernel)

    # Track interrupt events
    interrupt_count = [0]

    def handle_interrupt(core_id):
        """Interrupt handler callback"""
        print(f"Interrupt received from core {core_id}")
        interrupt_count[0] += 1

    # Set interrupt handler for all cores
    # Use 0xFFFFFFFF for all cores, or specific core ID
    device.set_interrupt_handler(handle_interrupt, core_id=0xFFFFFFFF)

    # Prepare data and run kernel
    input_buffer = device.allocate_and_copy_ndarray(np.array([1, 2, 3], dtype=np.int32))
    device.run_kernel("interrupt_entrypoint", [input_buffer])

    # Wait for interrupt and resume
    # The interrupt handler will be called automatically
    device.resume_kernel()

    print(f"Total interrupts: {interrupt_count[0]}")

    device.free(input_buffer)

if __name__ == "__main__":
    interrupt_example()

Kernel Parameter Registers

Read and write kernel parameter registers for fine control:

import quadric

## Write to kernel parameter register
device.write_kernel_parameter_register(
    quadric.ReservedKernelParams.ElsDependencyQueue,
    value=0,
    core_id=0
)

## Read from kernel parameter register
status = device.read_kernel_parameter_register(
    quadric.ReservedKernelParams.KernelInterruptStatus
)

print(f"Interrupt status: {status}")

Available reserved parameters:

  • quadric.ReservedKernelParams.ElsDependencyQueue
  • quadric.ReservedKernelParams.KernelInterruptStatus

Multi-Cluster Configuration

Advanced multi-cluster device setup:

import quadric

def multi_cluster_example():
    """
    Create and use a multi-cluster device for high-performance workloads
    """
    device_manager = quadric.DeviceManager()

    # Configure multi-cluster device
    cluster_size = 8      # Cores per cluster
    num_clusters = 4      # Total clusters

    config = quadric.DeviceConfiguration()
    config.ocm_size = 16 * 1024 * 1024  # 16 MB OCM per cluster
    config.profile = True

    # Create device
    device = device_manager.get_multicluster_device(
        cluster_size,
        num_clusters,
        config
    )

    print(f"Created device with {num_clusters} clusters")
    print(f"Each cluster has {cluster_size} cores")
    print(f"Total processing units: {cluster_size * num_clusters}")

    return device

if __name__ == "__main__":
    device = multi_cluster_example()

API Reference

Classes

DeviceManager

Manages device lifecycle and creation.

class DeviceManager:
    def __init__(self):
        """Create a new device manager"""

    def get_device(self, config: DeviceConfiguration) -> Device:
        """Create a single-core device"""

    def get_multicore_device(self, configs: list[DeviceConfiguration]) -> Device:
        """Create a multi-core device"""

    def get_multicluster_device(
        self,
        cluster_size: int,
        num_clusters: int,
        config: DeviceConfiguration
    ) -> Device:
        """Create a multi-cluster device"""

Device

Represents a device instance.

class Device:
    # Memory management
    def allocate(self, shape: list[int], dtype: np.dtype) -> DeviceBufferRef:
        """Allocate device memory"""

    def allocate_and_copy_ndarray(self, array: np.ndarray) -> DeviceBufferRef:
        """Allocate and copy NumPy array to device"""

    def free(self, buffer: DeviceBufferRef) -> None:
        """Free device memory"""

    # Data transfer
    def copy_ndarray_to_device(
        self,
        array: np.ndarray,
        buffer: DeviceBufferRef | DeviceBufferOffsetRef
    ) -> None:
        """Copy NumPy array to device"""

    def copy_ndarray_from_device(
        self,
        buffer: DeviceBufferRef | DeviceBufferOffsetRef,
        shape: list[int],
        dtype: np.dtype
    ) -> np.ndarray:
        """Copy data from device to NumPy array"""

    # Kernel operations
    def load_kernel(self, kernel: Kernel, offset: int = 0) -> None:
        """Load kernel to device"""

    def run_kernel(
        self,
        entrypoint: str,
        arguments: list[KernelArgument] = []
    ) -> None:
        """Execute kernel on all cores"""

    def run_kernel(
        self,
        entrypoint: str,
        core_id: int
    ) -> None:
        """Execute kernel on a specific core (multi-core devices only)"""

    def run_kernel(
        self,
        entrypoint: str,
        arguments: list[KernelArgument],
        core_id: int
    ) -> None:
        """Execute kernel with arguments on a specific core (multi-core devices only)"""

    def resume_kernel(self) -> None:
        """Resume kernel after interrupt"""

    # Profiling
    def print_profile(self, core_id: int = 0xFFFFFFFF) -> None:
        """Print profiling information"""

    # Interrupts
    def set_interrupt_handler(
        self,
        handler: callable,
        core_id: int = 0xFFFFFFFF
    ) -> None:
        """Set interrupt handler callback"""

    # Kernel parameters
    def write_kernel_parameter_register(
        self,
        parameter: ReservedKernelParams,
        value: int,
        core_id: int
    ) -> None:
        """Write to kernel parameter register"""

    def read_kernel_parameter_register(
        self,
        parameter: ReservedKernelParams
    ) -> int:
        """Read from kernel parameter register"""

    # Device info
    def get_device_type_string(self) -> str:
        """Get device type as string"""

DeviceConfiguration

Configuration for device creation.

class DeviceConfiguration:
    def __init__(self, num_cores: int = 8):
        """Create device configuration"""

    # Hardware architecture
    core_dim: int                    # Number of cores: 8, 16, 32
    border_width: int                # Border cores: 1 or 2
    mac_cfg: MacCfg                  # MACs per PE
    ocm_size: int                    # OCM size (bytes)
    four_bit_mode_en: WeightModeEnabled  # 4-bit weight mode
    inst_mem_depth: int              # Instruction memory (bytes)
    cycle_time_ns: float             # Cycle time (nanoseconds)

    # DDR configuration
    ddr_rd_bw_total: float           # Read bandwidth (Mbps)
    ddr_rd_avg_pct: float            # Average read utilization (0-100)
    ddr_rd_max_pct: float            # Max read utilization (0-100)
    ddr_rd_burst_limit: int          # Read burst limit
    ddr_avg_rd_latency: int          # Read latency (cycles)
    ddr_wr_bw_total: float           # Write bandwidth (Mbps)
    ddr_wr_avg_pct: float            # Average write utilization (0-100)
    ddr_wr_max_pct: float            # Max write utilization (0-100)
    ddr_wr_burst_limit: int          # Write burst limit
    ddr_avg_wr_latency: int          # Write latency (cycles)
    ddr_axi_width: int               # AXI width (bits)

    # OCM configuration
    ocm_rd_bw_total: float           # Read bandwidth (Mbps)
    ocm_rd_avg_pct: float            # Average read utilization (0-100)
    ocm_rd_max_pct: float            # Max read utilization (0-100)
    ocm_rd_burst_limit: int          # Read burst limit
    ocm_wr_bw_total: float           # Write bandwidth (Mbps)
    ocm_wr_avg_pct: float            # Average write utilization (0-100)
    ocm_wr_max_pct: float            # Max write utilization (0-100)
    ocm_wr_burst_limit: int          # Write burst limit

    # IFU configuration
    ifu_rd_bw_total: float           # Read bandwidth (Mbps)
    ifu_rd_avg_pct: float            # Average read utilization (0-100)
    ifu_rd_max_pct: float            # Max read utilization (0-100)
    ifu_rd_burst_limit: int          # Read burst limit
    ifu_avg_rd_latency: int          # Read latency (cycles)
    ifu_cache_line_width_bytes: int  # Cache line width
    ifu_cache_total_bytes: int       # Total cache size

    # Profiling and debug
    profile: bool                    # Enable profiling
    print_profile_cycles: bool       # Print cycle counts
    inf_bw: bool                     # Infinite bandwidth
    random_init: bool                # Random memory init
    seed: int                        # Random seed
    mem_trace_enable: bool           # Memory tracing
    mem_trace_filename: str          # Trace file
    inst_exec_counts_filename: str   # Instruction counts file
    event_stream_target: str         # Event streaming
    power_profile_target: str        # Power profiling
    power_profile_map_file: str      # Power profile mappings
    power_profile_report_period: int # Report period
    post_kernel_flow_timeout_cycles: int  # Flow timeout
    enable_ecc_cycle_modeling: bool  # ECC modeling
    cycle_count_heartbeat_target: str # Options: NONE, STDERR, STDOUT, <filename>
    cycle_count_heartbeat_period: int    # Report every N cycles

Enumerations

MacCfg

Available from quadric.host_python_bindings.MacCfg:

class MacCfg:
    M8 = ...   # 8 MACs per PE
    M16 = ...  # 16 MACs per PE
    M32 = ...  # 32 MACs per PE

WeightModeEnabled

Available from quadric.host_python_bindings.WeightModeEnabled:

class WeightModeEnabled:
    Disabled = ...  # 4-bit mode disabled
    Enabled = ...   # 4-bit mode enabled

ReservedKernelParams

class ReservedKernelParams:
    ElsDependencyQueue = ...      # ELS dependency queue
    KernelInterruptStatus = ...   # Kernel interrupt status

Functions

get_kernel

def get_kernel(filename: str) -> Kernel:
    """
    Load a compiled kernel from a binary file.

    Args:
        filename: Path to kernel WITHOUT the .qo extension
                 (the .qo extension is added automatically)

    Returns:
        Kernel object ready to load to device

    Example:
        # Correct: omit .qo extension
        kernel = quadric.get_kernel("path/to/my_kernel")

        # Incorrect: do not include .qo
        # kernel = quadric.get_kernel("path/to/my_kernel.qo")  # Wrong!
    """

Table of Contents


© Copyright 2026 Quadric All Rights Reserved • Privacy Policy
This documentation is preliminary and confidential. It is subject to change. Quadric does not give any warranty express or implied that the contents will be complete or accurate or up to date. The company shall not be liable for any loss, actions, claims, proceedings, demands or costs or damages whatsoever or howsoever caused arising directly or indirectly in connection with or arising out of the use of this material.

Sign in to your account

Don't have an account? Create an Account
By signing in, you are agreeing to our Terms of Use and Privacy Policy.

Develop.

Simulate.

Profile.

Collaborate.

We use cookies to enhance your browsing experience, and analyze our traffic.
By clicking "Accept All", you consent to our use of cookies.