Flows¶
In HLSFactory, different Flow classes are defined to perform certain build steps, run external vendor tools on a design, or process designs with custom logic. These classes encapsulate the logic for executing these steps. The abstract base class Flow defines the interface for all flows and outlines a common set of functions to execute flows in parallel across multiple designs and datasets.
Each subclass of the Flow class defines an abstract method called execute with the following function signature:
class Flow(ABC):
...
@abstractmethod
def execute(self, design: Design, timeout: float | None = None) -> list[Design]: ...
This method contains the core logic for each Flow subclass. For instance, the VitisHLSSynthFlow(ToolFlow) includes logic in its execute method to call the Vitis HLS synthesis tool on the design and return the resulting design object with the HLS synthesis results extracted and stored alongside the design source on disk.
The function signature might be confusing. For example, why would a flow that synthesizes a single design return a list of designs? There are two possible outcomes: 1) the synthesis is successful, and the design is returned as a list with a single element: [design], or 2) the synthesis fails or times out and returns an empty list: []. Other flows, like frontend flows, can elaborate a single base design into multiple new designs. In this case, the list of designs returned by the execute method would contain all the new designs created from the base design: [design_new_1, design_new_2, ...]. This pattern of returning a list of designs is used for all flows and has additional benefits, such as allowing ergonomic parallelization and combining the results of multiple designs and datasets executed in parallel.
We also include an optional timeout parameter in the execute method to allow user-defined flows to check for a timeout when calling and executing external tools, which may be long-running. For example, when exploring different combinations of optimization directives, a certain combination might lead a design to have unreasonably long synthesis times, such as by unrolling a huge loop or completely partitioning a large array. In this case, the user can set a timeout for the flow to stop the synthesis and return no results if the flow takes longer than the timeout.
Users will rarely use the execute function directly to call the flow on a single design. Instead, users will more commonly use the following helper functions of the Flow class to run the flow on a single design, a list of designs, or a dataset of designs:
def execute_multiple_designs(
self,
designs: list[Design],
n_jobs: int = 1,
cpu_affinity: list[int] | None = None,
timeout: float | None = None,
) -> list[Design]: ...
def execute_multiple_design_datasets_naive_parallel(
self,
design_datasets: DesignDatasetCollection,
copy_dataset: bool,
new_dataset_name_fn: Callable[[str], str] | None = None,
n_jobs: int = 1,
cpu_affinity: list[int] | None = None,
timeout: float | None = None,
) -> DesignDatasetCollection: ...
def execute_multiple_design_datasets_fine_grained_parallel(
self,
design_datasets: DesignDatasetCollection,
copy_dataset: bool,
new_dataset_name_fn: Callable[[str], str] | None = None,
n_jobs: int = 1,
cpu_affinity: list[int] | None = None,
par_chunksize: int | None = 1,
timeout: float | None = None,
) -> DesignDatasetCollection: ...
However, users typically call these functions using the default values for the optional parameters:
output_designs = my_flow.execute_multiple_designs(my_designs, n_jobs=N_JOBS)
my_datasets_v0 = my_flow.execute_multiple_design_datasets_naive_parallel(my_design_datasets, True, n_jobs=N_JOBS)
my_datasets_v1 = my_flow.execute_multiple_design_datasets_fine_grained_parallel(my_design_datasets, True, n_jobs=N_JOBS)
We always recommend using the execute_multiple_design_datasets_fine_grained_parallel function over the execute_multiple_design_datasets_naive_parallel function when running flows on multiple datasets, as it doesn’t block the execution of the flow for each dataset independently and results in faster overall execution time.
Frontend Flows vs. Tool Flows¶
In HLSFactory, we distinguish between Frontend(Flow) and ToolFlow(Flow). These classes don’t contain any logic themselves (literally just ...) but are used for better code organization in case of more complex code structures and flow features in the future.
However, there are fundamental functional differences in the logic that these two types of flows implement in their execute methods. Frontend flows generally take a single design and elaborate it into multiple new designs, while tool flows generally take a single design, run a tool on it, and return the same design but with the output of the tool stored alongside the design source.
Frontend Flows and OptDSL¶
As mentioned, frontend flows are used to generate multiple enumerated designs from a single base design. This can be as simple as templating a design with different values or as complex as custom source-to-source transformation of the HLS designs. The most common use case for frontend flows is to expand the design space of a single design by generating design variations of a base design with different combinations of optimization directives and values. For example, we may take a base design with a loop and generate four new designs, each with a loop unroll pragma and different unroll factors for the loop. This way, we have four different designs we can synthesize, each resulting in a different implementation.
Xilinx Flows¶
Siemens Catapult Flow¶
CatapultHLSSynthFlow runs the synthesis Tcl file declared by the design’s CatapultHLSSynthFlow configuration:
[[flow_configs]]
flow_name = "CatapultHLSSynthFlow"
synth_tcl = "synth.tcl"
The flow invokes catapult -shell -file synth.tcl in the design directory. A successful Tcl script must advance the solution through scheduling/assembly and extraction so Catapult produces cycle.rpt and rtl.rpt. HLSFactory parses those reports into data_hls.json, including:
Clock period and uncertainty
Latency, throughput, reset length, and initiation interval in cycles
Critical-path delay and slack
Operation count
Total, combinational, sequential, datapath, register, functional-unit, MUX, logic, memory, ROM, and FSM area metrics
The flow supports the same dataset-level parallel execution and per-design timeout interface as the other ToolFlow implementations. It creates error__CatapultHLSSynthFlow.txt or timeout__CatapultHLSSynthFlow.txt when a design fails, and records runtime and completion status in execution_data.json when execution-time logging is enabled.
The Catapult executable is resolved from an explicit catapult_bin, then
HLSFACTORY_CATAPULT_PATH, and finally PATH. The environment setting accepts
the executable or an installation directory containing bin/catapult or
Mgc_home/bin/catapult. Source the Siemens environment before starting Python
so worker processes inherit the license settings. See the
Catapult HLS tutorial for a complete example.
Google XLS Flow¶
XLSHLSSynthFlow converts a DSLX function or proc to XLS IR, optimizes the IR,
and lowers it to synthesizable Verilog. It invokes ir_converter_main,
opt_main, and codegen_main in the design directory, using the top and source
declared in hlsfactory.toml:
[[flow_configs]]
flow_name = "XLSHLSSynthFlow"
dslx_file = "adder.x"
top = "add"
generator = "pipeline"
pipeline_stages = "1"
delay_model = "unit"
The flow supports pipeline and combinational generators, stateful procs with an
explicit reset, per-design timeouts, parallel dataset execution, and the normal
HLSFactory error/timeout markers. Successful runs retain both IR forms, RTL, the
DSLX interface, module signature, schedule, scheduled and block IR, source-line
map, effective option snapshots, pass-pipeline metrics, and XLS block metrics.
data_hls.json records the configured codegen settings along with signature
latency/II, flop and feedthrough data, estimated path delays, aggregated
operation/BOM counts, and all artifact paths. Optimizer/codegen IR dumps and
pprof pass profiles are available through opt-in flow settings.
XLS is resolved from an explicit constructor argument, then HLSFACTORY_XLS_PATH,
then the HLSFactory server default /usr/scratch/common/xls. Both release-root
and Bazel bazel-bin/xls/ executable layouts are supported. See the
Google XLS tutorial for a complete dataset run and
custom-design example.