hls::array_partition
objects in your code include the header file
hls_array_partition.h
.
hls::array_partition
is a C++ library, provided as part of
AMD Vitis™ HLS
, that provides parallel array access with a guaranteed II=1,
even when HLS cannot prove at compile time that the accesses are conflict-free.
It is intended for the cases where the pragma HLS array_partition cannot deliver II=1 on its own, typically because the access pattern depends on a runtime value (for example, an index arriving from a stream) that HLS cannot resolve at compile time.
Two Usage Patterns
The library provides two ways to access a partitioned array in parallel:
- The standalone free functions hls::gather<T, K> and hls::scatter<T, K> operate on an existing T* array that you have partitioned yourself with #pragma HLS ARRAY_PARTITION cyclic factor=K. Use these for top-level function arguments, or arrays whose layout is managed elsewhere.
- The container class hls::array_partition<T, N, K> which wraps the array inside the class itself and applies the partitioning pragma automatically in its constructor. Use this for local (on-chip) arrays. With this usage the code is less verbose, and you don’t have to maintain the partition pragma manually.
Example: Simple Parallel Access using Standalone Free Functions
#include "hls_array_partition.h"
float func(float A[40], size_t i, size_t s) {
#pragma HLS ARRAY_PARTITION variable=A cyclic factor=4
auto v = hls::gather<float, 4>(A, {i+s*0, i+s*1, i+s*2, i+s*3});
return v.reduce_add();
}
The gather function first argument is the array itself, the second argument provides a list of indexes to access. It’s actually an hls::vector<> of the same size.
In this example, the variables “i" and “s” are potentially random and, without this library, the compiler would not allow 4 parallel accesses on A, thus preventing an initiation interval of 1. Depending on the physical implementation of A, this would lead to 4 accesses (single port) or 2 accesses (dual port).
Example: Container Class with Enable Mask
#include "hls_array_partition.h"
// K=4 parallel banks, N=40 elements of float
hls::array_partition<float, 40, 4u> buf;
// Load 4 values in parallel with an explicit index vector
hls::vector<float, 4> values = buf.gather({i+s*0, i+s*1, i+s*2, i+s*3});
// Store 4 values; the enable mask controls which lanes commit
buf.scatter({j, j+1, j+2, j+3}, values, {true, true, false, true});
This is similar to the previous example using the container class. As previously mentioned, there is no need for the array partition pragma as it’s embedded inside the class constructor, alongside the array itself.
The example also shows how the scatter method is used to store the values using the enable mask via and hls::vector<bool,K> with K=4.
Why the HLS Array Partition Library is Useful and a Word of Caution
Consider the either of the example above (they are equivalent) with a dynamic stride s.
Without the use the library, this is what happens:
- With cyclic partitioning and K=4, element A[x] lives in bank x % 4.
- When s is a runtime value, HLS cannot prove which bank each of A[i], A[i+s], A[i+2s], and A[i+3s] falls into, so it must assume they might collide — the four reads are scheduled one per cycle, giving II=4.
Now, hls::gather inverts this: instead of asking the compiler to prove conflict-freedom, you assert it yourself, and the library schedules all four reads as a single parallel access across the four banks in one cycle, giving II=1.
The one-time cost of that guarantee is that you , not the compiler, are responsible for choosing an s that never puts two of the four indices in the same bank : get it wrong and the hardware still reports II=1, with the wrong data in the colliding lanes.
When you run c-simulation there will be asserts that will trigger if you don’t respect the implied guarantee that there are no collisions. However the same warning applies as previously: if you get it wrong and the hardware runs with different values, you will get erroneous data in hardware due to the colliding lanes.
Class Reference: hls::array_partition<T, N, K>Let’s look at the main methods of the class. In the following, T is the element type, N is the total number of elements in the array, and K is the number of parallel lanes (the partition factor). K must be an unsigned compile-time constant.
gather(idx, en) — Load K Values in Parallel
hls::vector<T, K> gather(const hls::vector<size_t, K> &idx,
const hls::vector<bool, K> &en = hls::vector<bool, K>(true)) const;
Loads K values in parallel from the partitioned array. idx specifies the index to load from each lane; the indices do not need to be aligned to a bank boundary, but the enabled lanes must access different banks or a bank collision occurs. en is an optional enable mask (defaults to all lanes enabled and being read) allowing fewer than K lanes to be read in a given call. Returns the K values loaded, one per lane, in lane order.
gather(start_idx, n) — Load K Consecutive Values
hls::vector<T, K> gather(size_t start_idx,
size_t n = K) const;
Shorthand for loading K consecutive elements starting at start_idx. Equivalent to calling gather({start_idx, start_idx+1, …, start_idx+K-1}, {true, …, true}). The optional n parameter (default K) enables only the first n lanes, for use at loop boundaries where fewer than K elements remain.
scatter(idx, vals, en) — Store K Values in Parallel
void scatter(const hls::vector<size_t, K> &idx,
const hls::vector<T, K> &vals,
const hls::vector<bool, K> &en = hls::vector<bool, K>(true));
Stores K values in parallel to the partitioned array. idx specifies the index to store to for each lane (must access different banks among enabled lanes); vals supplies the K values to store. en is an optional enable mask (defaults to all lanes enabled) that controls which lanes actually commit their value.
scatter(start_idx, vals, n) — Store K Consecutive Values
void scatter(size_t start_idx,
const hls::vector<T, K> &vals,
size_t n = K);
Shorthand for storing K consecutive elements starting at start_idx. The optional n parameter (default K) enables only the first n lanes.
Standalone Free Functions
template<typename T, unsigned K>
hls::vector<T, K> hls::gather(const T *array,
const hls::vector<size_t, K> &idx,
const hls::vector<bool, K> &en = hls::vector<bool, K>(true));
template<typename T, unsigned K>
hls::vector<T, K> hls::gather(const T *array,
size_t start_idx,
size_t n = K);
template<typename T, unsigned K>
void hls::scatter(T *array,
const hls::vector<size_t, K> &idx,
const hls::vector<T, K> &vals,
const hls::vector<bool, K> &en = hls::vector<bool, K>(true));
template<typename T, unsigned K>
void hls::scatter(T *array,
size_t start_idx,
const hls::vector<T, K> &vals,
size_t n = K);
These free functions have the same semantics as the corresponding class methods, but operate on an array you have partitioned yourself: array must already have #pragma HLS ARRAY_PARTITION variable=<name> cyclic factor=<K> applied to it in the enclosing scope.
Constraints and Considerations
The library has four design constraints to be aware of before use.
- 1D arrays only. Multi-dimensional arrays must be linearized to 1D before use, for example A[row][col] becomes A[row * COLS + col].
- Cyclic partitioning only. The K banks are assigned round-robin, so bank[i] = i % K.
- Power-of-2 K is strongly recommended. A non-power-of-2 K requires the synthesized logic to compute a runtime modulo by K, which adds LUTs and latency that can outweigh the benefit of achieving II=1, especially for small arrays. A power-of-2 partitioning factor makes everything easier as division by a power-of-2 becomes a right shift of the bit values of the indexes, which is free in hardware.
- Bank conflicts are undefined behavior, and are silent at synthesis. The library assumes zero bank conflicts at runtime: if two enabled lanes in the same gather or scatter call access the same bank, C simulation and co-simulation report an assertion failure, but synthesis produces II=1 hardware with undefined values for the conflicting lanes — no synthesis-time error or warning is generated.
The conflict-free condition for a strided gather {i, i+s, i+2s, …, i+(K-1)s} is that gcd(s, K) == 1, where gcd is the greatest common divisor — the largest number that divides both s and K. For example, with K=4, any odd stride s guarantees conflict-free access, while s=2 or s=4 produce bank conflicts for at least one starting index i. in practice this means that s and K must not have common divisors other than the value 1.
When to Use hls::array_partition vs. Pragma HLS array_partition
#pragma HLS array_partition is the right first choice for most designs. HLS analyzes the array access pattern at compile time and, when it can prove that parallel loop iterations always access different banks, automatically generates a partitioned array and parallel memory structure with II=1. This requires no changes to your algorithm.
The HLS array partition library (hls::array_partition) is the right choice when the pragma cannot deliver II=1 because the access indices are not statically analyzable — for example, when indices arrive from a stream, are computed from runtime parameters, or follow a pattern that HLS's dependence analysis cannot resolve. In these cases, the pragma falls back to a less aggressive partition, and you will observe resource-conflict-induced II > 1 in the schedule viewer.
The library shifts the burden of proof from the compiler to you: you guarantee, by construction, that no two parallel accesses hit the same bank at runtime, and in return you get an II=1. The trade-off is that you must manually vectorize the affected code using hls::vector<T, K>, typically by restructuring the loop to process K iterations at a time – a manual unroll by factor K.
| Situation | Recommended approach |
| Indices are loop variables or constants | #pragma HLS array_partition |
| Indices are dynamic but conflict-free by construction | hls::array_partition library |
| Index source is an HLS stream | hls::array_partition library |
| Partition factor K is not a compile-time constant | Neither — requires redesign |