ztachip Hardware Architecture
1. ztachip (top)

1.1 Interfaces
| Name | Description |
|---|---|
| axilite_* | AXILite bus for RISCV to push tensor instructions to ztachip |
| axi_* | AXI bus for ztachip to initiate DMA memory transfer to/from external memory |
1.2 Subcomponents
| Name | Description |
|---|---|
| axilite | bridge to connect dp_core with RISCV via axiLite bus protocol. |
| sram_core | scratch memory block to hold temporary data is sometimes required during tensor data transfer |
| ddr_rx | Handling DMA transfer from external DDR memory to core's internal memory. |
| ddr_tx | Handling DMA transfer from core's internal memory to external DDR memory |
| core | This is the tensor arithmetic execution unit which is composed of an array of lightweight VLIW processors. |
| dp_core | This is the central tensor processor unit that coordinates all activities within ztachip including memory transfer and launching execution on the VLIW processor array. |
| fpu | Handling tensor floating point arithmetics (FLOAT32/BFLOAT) and aggregate arithmetics such as MAX,SUM,DOT-PRODUCT-SUM. |
1.3 Functions
This is the top-level component of ztachip.
The central tensor processor unit is dp_core
dp_core receives tensor instructions from RISCV via axilite_* interface.
dp_core then executes the tensor instructions by performing the following:
- Coordinating tensor data operations which transfer tensor data between sram_core,
core's internal memory and external DDR memory. Tensor data operations may also include other complex functions such as transpose, dimension resize, data-remap...
2. ztachip.dp_core

2.1 Interfaces
| Name | Description |
|---|---|
| bus_* | bus for dp_core to receive tensor instructions from RISCV. |
| task_* | bus to send tensor operator execution instructions to core |
| readmaster1* | bus to receive DMA data transfer from core's internal memory. |
| readmaster2* | bus to receive DMA data transfer from sram_core's scratch-pad memory |
| readmaster3* | bus to receive DMA data transfer from external DDR memory through ddr_rx |
| writemaster1* | bus to send DMA data transfer to core's internal memory. |
| writemaster2* | bus to send DMA data transfer to sram_core's scratch-pad memory. |
| writemaster3* | bus to send DMA data transfer to external DDR memory through ddr_tx |
2.2 Subcomponents
| Name | Description |
|---|---|
| dp_fetch | This component receives tensor instructions from RISCV and then dispatch them at the right time. Tensor data operations are dispatched to dp_gen_core. Tensor operator execution is dispatched to core via task* interface signals. |
| dp_gen_core | This component receives tensor data operation instructions from dp_fetch. It then generates the memory addresses for the transfer. There can be 2 tensor data operations executing at the same time with each assigned to one of the two dp_gen subcomponents. |
| dp_source | This component is responsible for generating the DMA transfer read requests to the source of the tensor data operations. It then forwards the received data to the appropriate dp_sink components which are then responsible for the DMA transfer of the received data to the destination point. |
| dp_sink | This component is responsible for generating DMA transfer write request to the destination point of the tensor data operations. It receives the data and addresses information for the transfer from the appropriate dp_source above. |
2.3 Functions
This component is the main tensor processor of ztachip. It performs the following functions:
- dp_fetch receives tensor instructions from RISCV. Each tensor instruction are associated
with a hardware thread. There are 2 hardware threads available. Hardware threads are useful to provide the ability to overlay the tensor operator execution phase of one thread with the data transfer phase of the other threads.
- Decodes the tensor instructions.
- Tensor instructions may be executed out-of-order but applications can enforce the order.
- Data operations are then processed by dp_gen_core,
dp_source and dp_sink. There can be up to 2 data operations executing at the same time. For example, data transfer from the core's internal memory to DDR external memory can occur at the same time as data transfer from the scratch-pad to core's internal memory.
- Tensor operator execution is forwarded to core via interface signal task*
- Before tensor operator execution can be issued, all memory transfer with core's internal memory must be completed. Since there is a separate core's internal memory for each hardware thread, for example, memory transfer to core's internal memory belonging to thread#1 can still be running at the same time while core is busy performing tensor operator execution but on thread#2.
3. ztachip.fpu

3.1 Interfaces
| Name | Description |
|---|---|
| bus* | AXI interface for RISCV to push FPU instructions to FPU. |
| fpu_read* | Read interface to sram_core. FPU operates only from SRAM memory. Data input for FPU must first be transferred from DDR or PCORE memory space to SRAM space by DP instructions. |
| fpu_write* | Write interface to sram_core. FPU results are written only to SRAM memory. Results must then be transferred to DDR or PCORE memory space from SRAM space by DP instructions. |
3.2 Subcomponents
| Name | Description |
|---|---|
| cmd_fifo | holds FPU instructions issued from RISCV. Each FPU instruction is associated with a HART context. There are 2 cmd_fifo for each of the HART context. |
| B_fifo_i | used to store B parameters associated with B+C*X*Y opcode. FPU executions are pipelined with all data input being prefetched and pipelined. |
| X_fifo_i | used to store X parameters associated with B+C*X*Y opcode. FPU executions are pipelined with all data input being prefetched and pipelined. |
| Y_fifo_i | used to store Y parameters associated with B+C*X*Y opcode. FPU executions are pipelined with all data input being prefetched and pipelined. |
| falu_vector | Perform FPU operations in vector mode by instantiating an array of falu_core. |
| falu_core | Perform FPU operations for each vector elements |
| falu | performs all falu_core functions except for the SUM opcode which is executed by falu2 instead. |
| falu2 | performs the SUM part after the MAC operations from falu. For example with the operation SUM(X*Y*C), the X*Y*C is performed by falu and results from falu are summed together by falu2 |
3.3 Functions
FPU supports floating point arithmetic with different precision below
- FP32: Many transformer intermediate calculations should stay in FP32
- BFLOAT: Transformer activations are normally stored in BFLOAT
Aggregate functions are also better supported by FPU. Aggregate functions are inherently serial operations and better supported with FPU's data flow architecture instead. The following aggregate functions are supported
- MAX
- SUM
- DOT-PRODUCT-SUM
FPU operates in vector mode. FPU input/output are vectors of float numbers. FPU vector width is configured in config.vhd as fpu_gen_max_c.
FPU execution is fully pipelined. For example, for the all important FMA operations found in LLM attention stage, it can execute up to 2*fpu_gen_max_c float operations (FMA=1ADD+1MUL) per clock cycle.
Data fetching is streamed to FPU from SRAM. Data fetching are overlapped with execution, therefore adding no delay to FPU execution.
FPU operates from SRAM memory space only
- FPU data input must first be transferred from PCORE or DDR space to SRAM space by DP instructions
- FPU results must be transferred to PCORE or DDR space from SRAM space by DP instructions
For highest performance, FPU should be used under both HART context. There is 1 FPU hardware instance but it can operate under different HART context. One can use one HART context to transfer data between SRAM and DDR/PCORE while the other HART context is still busy executing FPU instructions. This way, all memory overhead are overlapped with FPU execution.
4. ztachip.core

4.1 Interfaces
| Name | Description |
|---|---|
| dp_write* | bus to receive DMA data transfer to its internal memory |
| dp_read* | bus to send DMA data transfer from its internal memory |
| task* | bus to receive tensor operator execution commands from dp_core |
4.2 Subcomponents
| Name | Description |
|---|---|
| stream | this is a stream processor that performs data mapping between input and output. One stream processor is used to perform data mapping before data is written to core's internal memory And a second stream processor performs data mapping on data as it is just retrieved from core's internal memory. |
| cell | 4 pcore are grouped in a cell. The purpose is to improve the fan-out performance of bus signals. |
| cell.pcore | Implements the VLIW processor array. All tensor operator execution are performed by many ot these pcores. |
| instr | This is the master processor for all pcore's VLIW processor cores that are just simply ALUs running in locked step mode with each other. |
| instr.instr_fetch | This component performs thread scheduling of execution. pcore are multi-threaded processors with 16 hardware threads executing in a round-robin fashion. VLIW instructions are complex and require a very deep pipeline, hardware multi-threading is a commonly used technique to hide the impact of instruction latency. Performance of 1 VLIW instruction per clock per pcore can then be achieved. |
| instr.rom | Holding VLIW instruction code. All VLIW cores are sharing the same instruction code. Since VLIW processors are all running in lock-step, only 1 instruction is fetched for all the VLIW processors at every clock. |
4.3 Functions
- This component is responsible for all tensor operator execution tasks.
- It is composed of an array of pcores. And each pcore is a VLIW vector processor.
- Requests for tensor operator execution are coming from dp_core via task* interface signal.
- The component instr is controlling the execution of all pcore's VLIW processors.
VLIW processors are very lightweight processors that are mostly just ALU with memory running in lock-step mode with each other.
5. ztachip.core.stream

This module provides an arbitrary data mapping between input value and output.
stream performs data mapping on data just before it is written to pcore's memory space.
stream also performs data mapping on data just after it is read from pcore's memory space.
The data mapping is realized using the formula below
y = lookup_quotient[x[11:5]] + x[4:0]* lookup_remainder[x[4:0]]
Effectively, the formula above provides a multi-point linear-interpolation between input and output.
stream provides up to 4 different data-mapping functions at the same time. The parameter stream_id_in selects which data mapping to be performed.
stream is very commonly used in AI applications to perform activation functions as data being written back to memory.
6. ztachip.core.pcore

6.1 Interfaces
| Name | Description |
|---|---|
| dp_read* | bus to send DMA data transfer from pcore's internal memory |
| dp_write* | bus to send DMA data transfer to pcore's internal memory |
| instruction* | bus to receive VLIW instructions to be executed. |
6.2 Subcomponents
| Name | Description |
|---|---|
| alu | This is the unit that performs the bulk of arithmetic vector calculation. There are 8 ALUs for each of the 8 elements in a vector word. |
| xregister_file | Register banks holding int32 accumulators used by FMA operations. |
| register_bank | Holding internal memory. All computations are operated directly from this internal memory without intermediate register load/store operations like more traditional processor design. |
| register_bank.register_file | There are 2 pages of internal memory with each page associated with one of the two tensor processor's hardware threads. The two register_file implement the two internal memory pages. |
| instr_decoder2 | Main decoder for VLIW instructions. |
| instr_dispatch2 | Component that interfaces between instr_decoder2 and bank of alu. It forwards execution instructions from instr_decoder2 to alu. It is also responsible for moving data from register_bank to alu. It is responsible for saving results from alu back to register_bank. |
| ialu | VLIW may also contain integer arithmetic operation for tasks such as loop counter, array indexing calculation, address calculation. This component is responsible for such calculation. |
| iregister_file | Register banks holding integer values used by ialu. |
6.3 Functions
This component performs tensor operator execution.
There are many instances of pcore running in parallel.
pcore has a VLIW architecture.
pcore execution is multi-threaded. There are 16 hardware threads of execution running in round-robin fashion.
The VLIW instruction is very wide that contains many sub-functions that perform the following tasks simultaneously within a single VLIW instruction.
- Address calculation for 2 input parameters and 1 output parameter.
- Fetching input parameter vectors from register_bank.
- Fetching 32-bit accumulator from xregister_file. These accumulator values are then used by alu performing FMA (Fused-multiply-add) operations.
- Performing integer calculations on ialu.
- Perform vector calculation on the bank of alu.
- alu may produce results as 32-bit accumulator, save these values to xregister_file.
- alu may produce results as 12-bit word vector. Save these results back to
Since all the functions above are simultaneously performed in one single VLIW instruction, the instruction pipeline is as long as 14 clocks. However, with hardware multi-threading execution, each stage of the execution pipeline is occupied by different threads. Therefore we have an effective execution rate of one VLIW instruction per clock per pcore.
For example, the code below is compiled into one single VLIW instruction. All data operations and arithmetic calculation together take just one clock of execution and also without any memory stall cycles. This same code using traditional RISCV instructions would take up to 10 RISCV instructions that may also incur memory stall cycles in addition.
z[i++] = x[i+2]+y[i+3];
7. ztachip.core.pcore.register_bank
This component implements pcore internal memory
pcore's internal memory is partitioned into 2 pages with each page assigned to one of the two tensor processor's threads. This is an important concept for ztachip since it allows for one thread to access its internal memory while the other thread is performing tensor operator execution using the other internal memory's page. This allows for a memory operation to be overlapped with tensor operator execution.
It is important to note that tensor operator execution operates only on internal memory without any reference to external memory. This is an important concept since with ztachip, data operations are decoupled from execution operations by having separate tensor instructions for data operations and execution operations. Reference here for further explanation of this concept.
Internal memory holds 2 types of data
- Private data: Data is private to each of the 16 threads. Private memory words are interleaved between different threads as shown in the picture below.
- Shared data: Data is shared among all 16 threads but within the same pcore. Shared memory words are allocated from the bottom up as shown in the picture below.
+-------------------------+
+ Thread1.Private word#0 |
+-------------------------+
+ Thread2.Private word#0 |
+-------------------------+
:
:
+-------------------------+
+ Thread16.Private word#0 |
+-------------------------+
+ Thread1.Private word#1 |
+-------------------------+
+ Thread2.Private word#1 |
+-------------------------+
:
:
+-------------------------+
+ Thread16.Private word#1 |
+-------------------------+
:
:
+-------------------------+
+ Shared word#3 |
+-------------------------+
+ Shared word#2 |
+-------------------------+
+ Shared word#1 |
+-------------------------+
+ Shared word#0 |
+-------------------------+
8. ztachip.core.pcore.instr_decoder2
This is the main component that decodes the computing components of VLIW instructions
There are 3 main operations to each VLIW instruction:
- operation on pcore.alu: To perform math operations on vector data
- operation on pcore.ialu: To perform simple scalar integer operation.
- branching operation. But this is performed by master processor core.instr since
all VLIW processors are running in lockstep therefore sharing the same branching operation. ztachip currently does not support yet thread branching/divergence as commonly implemented in GPU, therefore all threads must have the same branching condition. However you can still perform the equivalent of branching with conditional execution using VMASK register as described in next section. Most AI/Vision processing tasks however are mostly predictable computing with minimum or no branching.
VLIW instructions are 128 bit long and have the following format
MU -- pcore.alu instruction
oc 5 -- opcode
save 1 -- Save alu.y_out=>xregister_file
x3 -- x3 parameter;map to alu.xscalar_in
vector 1 -- 1 if parameter is vector,0 if scalar
addr 12 -- Internal memory address of x3
attr 4 -- Attribute
x1 -- x1 parameter;map to alu.x1_in
vector 1 -- 1 if parameter is vector,0 if scalar
addr 12 -- Internal memory address of x1
attr 4 -- Attribute
x2 -- x2 parameter;map to alu.x2_in
vector 1 -- 1 if parameter is vector,0 if scalar
addr 12 -- Internal memory address of x2
attr 4 -- Attribute
y -- y parameter;map to alu.y*_out
vector 1 -- 1 if parameter is vector,0 if scalar
addr 12 -- Internal memory address of y
attr 4 -- Attribute
IMU -- pcore.ialu instruction
oc 5 -- opcode
x1 4 -- x1 parameter; map to ialu.x1_in
x2 4 -- x2 parameter; map to ialu.x2_in
y 4 -- y parameter; map to ialu.y_out
const 13 -- Constant field
CTRL
oc 5 -- Branching opcode based on IMU.y value
addr 11 -- Code address to jump to
MU.attr field above provides many ways for parameters to reference memory.
Table below shows different addressing options for parameters.
where IREG is ialu's register file and MEM is pcore's internal memory.
ATTR DESCRIPTION MEMORY ACCESS PATTERN
===========================================================
11RR Access via pointer+index MEM[IREG[addr]+IREG[RR]]
1011 Access via pointer MEM[IREG[addr]]
1000 Access shared memory MEM[addr]
1001 Access private memory MEM[addr]
00RR Access shared memory+index MEM[addr+IREG[RR]]
01RR Access private memory+index MEM[addr+IREG[RR]]
9. ztachip.core.pcore.ialu
This component is responsible for the scalar integer calculation of VLIW instruction described above.
ialu operates using the following registers
- R0-R7 : General purpose integer registers.
- VMASK : Register with each bit is used to enable/disable a vector lane.
The integer calculations provided by ialu are typically used for tasks such as
- Loop counter/control
- Index to an array
- Memory reference by pointer
- Address calculation
- Enable/disable vector lanes.
For example
// i is an integer operation used for loop counter
for(i=0;i < 10;i++) {
:
:
}
// i is an integer used for array indexing and address calculation
z[i]=x[i]+y[i];
// p is an integer used for pointer reference to memory
// Both integers p and i are used for memory reference and
// address calculation below
p=&mem[0];
p[i]=10;
// VMASK=3 means only element#0 and element#1 of vector z are
// being updated after the vector calculation
VMASK=3;
z=x+2; // Only the first 2 elements of z are written back to memory.
10. ztachip.core.pcore.alu

10.1 Interfaces
| Name | Description |
|---|---|
| x1_in | This is a 12 bit input value. This parameter is fetched from pcore internal memory stored in register_file. |
| x2_in | This is 12 bit input value. This parameter is fetched from pcore internal memory stored in register_file |
| xreg_in | 32-bit accumulator input. This is fetched from xregister_file. |
| xscalar_in | constant used to specify the distance for shifting operation. |
| y_out | Results as a full 32-bit value. This is then be stored in xregister_file. |
| y3_out | Results after being clipped to 12 bit. Value is properly clipped if exceeding range. This is then be stored in register_file. |
| y2_out | Result of comparator against y_out value. The comparator tests y_out for various boolean conditions. This is then be stored in xregister_file. |
10.2 Functions
ALU has a fairly simple set of operations. We want the ALU to be as simple as possible to reduce resource requirements. They are meant to perform linear arithmetic operations occurring normally during AI and vision processing tasks.
Other non-linear operations are taken care of by stream.
ALU and stream together were shown to be able to cover a wide range of AI and vision processing.
ALU has the following arithmetic blocks:
- Multiplication of two 12 bit values.
- Adding the result of multiplication to a 32-bit accumulator value.
- Perform shift operation
- Perform boolean comparison of the result. Comparison against zero include GT,GE,LT,LE,EQ,NE
- Casting the 32-bit result to 12 bit result. Value is clipped if it is overflowed or underflowed after casting.
With the above blocks, the following opcodes are supported
COMPARE_LT : y2_out=(y_out < 0)?1:0
COMPARE_LE : y2_out=(y_out <= 0)?1:0
COMPARE_GT : y2_out=(y_out > 0)?1:0
COMPARE_GE : y2_out=(y_out >= 0)?1:0
COMPARE_EQ : y2_out=(y_out==0)?1:0
COMPARE_NE : y2_out=(y_out!=0)?1:0
MULTIPLY : y_out=x1*x2
ADD : y_out=x1+x2
SUBTRACT : y_out=x1-x2
FMA : xreg_in += x1_in*x2_in
FMS : xreg_in -= x1_in*x2_in
ASSIGN : y_out=x1_in
ACCUMULATOR_SHL : y_out=(xreg_in << x_scalar_in)
ACCUMULATOR_SHR : y_out=(xreg_in >> x_scalar_in)
INT12 SHR : y_out=(x1_in >> x_scalar_in)
INT12 SHL : y_out=(x1_in << x_scalar_in)