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
- Quick Start
- Installation & Import
- Device Management
- Device Configuration
- Memory Management
- Kernel Operations
- Data Transfer
- Complete Examples
- Advanced Features
- 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 bufferDeviceBufferOffsetRef: 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.ElsDependencyQueuequadric.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!
"""