holoso 0.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- holoso-0.1.0/PKG-INFO +16 -0
- holoso-0.1.0/README.md +107 -0
- holoso-0.1.0/holoso/__init__.py +48 -0
- holoso-0.1.0/holoso/_api.py +163 -0
- holoso-0.1.0/holoso/_backend/__init__.py +4 -0
- holoso-0.1.0/holoso/_backend/cocotb.py +195 -0
- holoso-0.1.0/holoso/_backend/html/__init__.py +3 -0
- holoso-0.1.0/holoso/_backend/html/_html.py +168 -0
- holoso-0.1.0/holoso/_backend/html/_schedule.py +851 -0
- holoso-0.1.0/holoso/_backend/html/html.css +121 -0
- holoso-0.1.0/holoso/_backend/html/html.js +293 -0
- holoso-0.1.0/holoso/_backend/numerical.py +366 -0
- holoso-0.1.0/holoso/_backend/verilog/__init__.py +3 -0
- holoso-0.1.0/holoso/_backend/verilog/_emit.py +905 -0
- holoso-0.1.0/holoso/_backend/verilog/_microcode.py +368 -0
- holoso-0.1.0/holoso/_backend/verilog/_support.py +79 -0
- holoso-0.1.0/holoso/_backend/verilog/rtl/README.md +7 -0
- holoso-0.1.0/holoso/_backend/verilog/rtl/holoso_support.vh +26 -0
- holoso-0.1.0/holoso/_backend/verilog/rtl/holoso_support_template.v +247 -0
- holoso-0.1.0/holoso/_backend/verilog/rtl/kulibin/README.md +32 -0
- holoso-0.1.0/holoso/_backend/verilog/rtl/kulibin/_tables/_zkf_cordic_m11.v +103 -0
- holoso-0.1.0/holoso/_backend/verilog/rtl/kulibin/_tables/_zkf_cordic_m16.v +105 -0
- holoso-0.1.0/holoso/_backend/verilog/rtl/kulibin/_tables/_zkf_cordic_m18.v +106 -0
- holoso-0.1.0/holoso/_backend/verilog/rtl/kulibin/_tables/_zkf_cordic_m24.v +109 -0
- holoso-0.1.0/holoso/_backend/verilog/rtl/kulibin/_tables/_zkf_cordic_m27.v +111 -0
- holoso-0.1.0/holoso/_backend/verilog/rtl/kulibin/_tables/_zkf_cordic_m32.v +113 -0
- holoso-0.1.0/holoso/_backend/verilog/rtl/kulibin/_tables/_zkf_cordic_m36.v +115 -0
- holoso-0.1.0/holoso/_backend/verilog/rtl/kulibin/_tables/_zkf_cordic_m48.v +121 -0
- holoso-0.1.0/holoso/_backend/verilog/rtl/kulibin/_tables/_zkf_cordic_m53.v +124 -0
- holoso-0.1.0/holoso/_backend/verilog/rtl/kulibin/_tables/_zkf_exp2_m16.v +161 -0
- holoso-0.1.0/holoso/_backend/verilog/rtl/kulibin/_tables/_zkf_exp2_m18.v +161 -0
- holoso-0.1.0/holoso/_backend/verilog/rtl/kulibin/_tables/_zkf_exp2_m24.v +353 -0
- holoso-0.1.0/holoso/_backend/verilog/rtl/kulibin/_tables/_zkf_exp2_m27.v +161 -0
- holoso-0.1.0/holoso/_backend/verilog/rtl/kulibin/_tables/_zkf_exp2_m32.v +225 -0
- holoso-0.1.0/holoso/_backend/verilog/rtl/kulibin/_tables/_zkf_exp2_m36.v +353 -0
- holoso-0.1.0/holoso/_backend/verilog/rtl/kulibin/_tables/_zkf_exp2_m48.v +161 -0
- holoso-0.1.0/holoso/_backend/verilog/rtl/kulibin/_tables/_zkf_exp2_m53.v +225 -0
- holoso-0.1.0/holoso/_backend/verilog/rtl/kulibin/_tables/_zkf_log2_m16.v +216 -0
- holoso-0.1.0/holoso/_backend/verilog/rtl/kulibin/_tables/_zkf_log2_m18.v +216 -0
- holoso-0.1.0/holoso/_backend/verilog/rtl/kulibin/_tables/_zkf_log2_m24.v +487 -0
- holoso-0.1.0/holoso/_backend/verilog/rtl/kulibin/_tables/_zkf_log2_m27.v +216 -0
- holoso-0.1.0/holoso/_backend/verilog/rtl/kulibin/_tables/_zkf_log2_m32.v +487 -0
- holoso-0.1.0/holoso/_backend/verilog/rtl/kulibin/_tables/_zkf_log2_m36.v +849 -0
- holoso-0.1.0/holoso/_backend/verilog/rtl/kulibin/_tables/_zkf_log2_m48.v +306 -0
- holoso-0.1.0/holoso/_backend/verilog/rtl/kulibin/_tables/_zkf_log2_m53.v +487 -0
- holoso-0.1.0/holoso/_backend/verilog/rtl/kulibin/_zkf_cordic.v +313 -0
- holoso-0.1.0/holoso/_backend/verilog/rtl/kulibin/_zkf_div_core.v +276 -0
- holoso-0.1.0/holoso/_backend/verilog/rtl/kulibin/_zkf_fixed_to_float.v +164 -0
- holoso-0.1.0/holoso/_backend/verilog/rtl/kulibin/_zkf_horner.v +141 -0
- holoso-0.1.0/holoso/_backend/verilog/rtl/kulibin/_zkf_normshift.v +281 -0
- holoso-0.1.0/holoso/_backend/verilog/rtl/kulibin/_zkf_pack.v +226 -0
- holoso-0.1.0/holoso/_backend/verilog/rtl/kulibin/_zkf_pmul.v +511 -0
- holoso-0.1.0/holoso/_backend/verilog/rtl/kulibin/_zkf_rshift_sticky.v +197 -0
- holoso-0.1.0/holoso/_backend/verilog/rtl/kulibin/_zkf_to_fixpoint.v +336 -0
- holoso-0.1.0/holoso/_backend/verilog/rtl/kulibin/zkf_abs.v +12 -0
- holoso-0.1.0/holoso/_backend/verilog/rtl/kulibin/zkf_add.v +520 -0
- holoso-0.1.0/holoso/_backend/verilog/rtl/kulibin/zkf_addsub.v +60 -0
- holoso-0.1.0/holoso/_backend/verilog/rtl/kulibin/zkf_atan2.v +897 -0
- holoso-0.1.0/holoso/_backend/verilog/rtl/kulibin/zkf_cmp.v +71 -0
- holoso-0.1.0/holoso/_backend/verilog/rtl/kulibin/zkf_cmp_comb.v +63 -0
- holoso-0.1.0/holoso/_backend/verilog/rtl/kulibin/zkf_div.v +123 -0
- holoso-0.1.0/holoso/_backend/verilog/rtl/kulibin/zkf_exp2.v +305 -0
- holoso-0.1.0/holoso/_backend/verilog/rtl/kulibin/zkf_fma.v +556 -0
- holoso-0.1.0/holoso/_backend/verilog/rtl/kulibin/zkf_from_int.v +127 -0
- holoso-0.1.0/holoso/_backend/verilog/rtl/kulibin/zkf_is_finite.v +11 -0
- holoso-0.1.0/holoso/_backend/verilog/rtl/kulibin/zkf_log2.v +438 -0
- holoso-0.1.0/holoso/_backend/verilog/rtl/kulibin/zkf_mul.v +156 -0
- holoso-0.1.0/holoso/_backend/verilog/rtl/kulibin/zkf_mul_ilog2_const.v +203 -0
- holoso-0.1.0/holoso/_backend/verilog/rtl/kulibin/zkf_neg.v +14 -0
- holoso-0.1.0/holoso/_backend/verilog/rtl/kulibin/zkf_pipe.v +54 -0
- holoso-0.1.0/holoso/_backend/verilog/rtl/kulibin/zkf_resize.v +195 -0
- holoso-0.1.0/holoso/_backend/verilog/rtl/kulibin/zkf_round.v +239 -0
- holoso-0.1.0/holoso/_backend/verilog/rtl/kulibin/zkf_saturate.v +19 -0
- holoso-0.1.0/holoso/_backend/verilog/rtl/kulibin/zkf_sincos.v +673 -0
- holoso-0.1.0/holoso/_backend/verilog/rtl/kulibin/zkf_sort.v +69 -0
- holoso-0.1.0/holoso/_backend/verilog/rtl/kulibin/zkf_to_int.v +126 -0
- holoso-0.1.0/holoso/_errors.py +44 -0
- holoso-0.1.0/holoso/_frontend/__init__.py +3 -0
- holoso-0.1.0/holoso/_frontend/_aggregate.py +87 -0
- holoso-0.1.0/holoso/_frontend/_ast_support.py +106 -0
- holoso-0.1.0/holoso/_frontend/_lower.py +2045 -0
- holoso-0.1.0/holoso/_frontend/_scope.py +87 -0
- holoso-0.1.0/holoso/_hir/__init__.py +40 -0
- holoso-0.1.0/holoso/_hir/_const.py +38 -0
- holoso-0.1.0/holoso/_hir/_const_fold.py +62 -0
- holoso-0.1.0/holoso/_hir/_copy.py +163 -0
- holoso-0.1.0/holoso/_hir/_dce.py +41 -0
- holoso-0.1.0/holoso/_hir/_if_convert.py +128 -0
- holoso-0.1.0/holoso/_hir/_ir.py +396 -0
- holoso-0.1.0/holoso/_hir/_operators.py +329 -0
- holoso-0.1.0/holoso/_hir/_optimize.py +23 -0
- holoso-0.1.0/holoso/_hir/_strength_reduce.py +88 -0
- holoso-0.1.0/holoso/_hir/_thread_merges.py +144 -0
- holoso-0.1.0/holoso/_hir/_types.py +31 -0
- holoso-0.1.0/holoso/_lir/__init__.py +52 -0
- holoso-0.1.0/holoso/_lir/_bankalloc.py +715 -0
- holoso-0.1.0/holoso/_lir/_build.py +314 -0
- holoso-0.1.0/holoso/_lir/_build_base.py +84 -0
- holoso-0.1.0/holoso/_lir/_coalesce.py +244 -0
- holoso-0.1.0/holoso/_lir/_construct.py +284 -0
- holoso-0.1.0/holoso/_lir/_ir.py +1276 -0
- holoso-0.1.0/holoso/_lir/_layout.py +287 -0
- holoso-0.1.0/holoso/_lir/_liveness.py +188 -0
- holoso-0.1.0/holoso/_lir/_mir_facts.py +134 -0
- holoso-0.1.0/holoso/_lir/_portassign.py +202 -0
- holoso-0.1.0/holoso/_lir/_ports.py +77 -0
- holoso-0.1.0/holoso/_lir/_regalloc.py +295 -0
- holoso-0.1.0/holoso/_lir/_schedule.py +279 -0
- holoso-0.1.0/holoso/_mir/__init__.py +33 -0
- holoso-0.1.0/holoso/_mir/_interpret.py +233 -0
- holoso-0.1.0/holoso/_mir/_ir.py +698 -0
- holoso-0.1.0/holoso/_mir/_lower.py +428 -0
- holoso-0.1.0/holoso/_operators.py +713 -0
- holoso-0.1.0/holoso/_type.py +236 -0
- holoso-0.1.0/holoso/_util.py +42 -0
- holoso-0.1.0/holoso/_value.py +291 -0
- holoso-0.1.0/holoso/py.typed +0 -0
- holoso-0.1.0/holoso.egg-info/PKG-INFO +16 -0
- holoso-0.1.0/holoso.egg-info/SOURCES.txt +146 -0
- holoso-0.1.0/holoso.egg-info/dependency_links.txt +1 -0
- holoso-0.1.0/holoso.egg-info/requires.txt +14 -0
- holoso-0.1.0/holoso.egg-info/top_level.txt +1 -0
- holoso-0.1.0/pyproject.toml +59 -0
- holoso-0.1.0/setup.cfg +4 -0
- holoso-0.1.0/tests/test_arithmetic_behavior.py +553 -0
- holoso-0.1.0/tests/test_backend.py +439 -0
- holoso-0.1.0/tests/test_const_install.py +45 -0
- holoso-0.1.0/tests/test_cosim.py +592 -0
- holoso-0.1.0/tests/test_cosim_examples.py +53 -0
- holoso-0.1.0/tests/test_cycle_model.py +186 -0
- holoso-0.1.0/tests/test_determinism.py +132 -0
- holoso-0.1.0/tests/test_example_reference.py +89 -0
- holoso-0.1.0/tests/test_frontend.py +2211 -0
- holoso-0.1.0/tests/test_fuzz.py +149 -0
- holoso-0.1.0/tests/test_fuzz_regressions.py +93 -0
- holoso-0.1.0/tests/test_install_landing.py +110 -0
- holoso-0.1.0/tests/test_interpret.py +171 -0
- holoso-0.1.0/tests/test_language_features.py +747 -0
- holoso-0.1.0/tests/test_latency_freeze.py +140 -0
- holoso-0.1.0/tests/test_metrics.py +259 -0
- holoso-0.1.0/tests/test_overlap_behavior.py +506 -0
- holoso-0.1.0/tests/test_passes.py +628 -0
- holoso-0.1.0/tests/test_public_api_behavior.py +559 -0
- holoso-0.1.0/tests/test_report.py +120 -0
- holoso-0.1.0/tests/test_schedule.py +2434 -0
- holoso-0.1.0/tests/test_synth.py +159 -0
- holoso-0.1.0/tests/test_timing_equivalence_behavior.py +224 -0
- holoso-0.1.0/tests/test_verify.py +1657 -0
holoso-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: holoso
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: A simple Python-to-Verilog synthesizer for numeric kernels, control systems, and DSP.
|
|
5
|
+
Requires-Python: >=3.14
|
|
6
|
+
Requires-Dist: numpy~=2.4
|
|
7
|
+
Requires-Dist: scipy~=1.17
|
|
8
|
+
Requires-Dist: sympy~=1.14
|
|
9
|
+
Provides-Extra: test
|
|
10
|
+
Requires-Dist: cocotb~=2.0; extra == "test"
|
|
11
|
+
Requires-Dist: jaxtyping~=0.3; extra == "test"
|
|
12
|
+
Requires-Dist: pytest~=9.0; extra == "test"
|
|
13
|
+
Provides-Extra: format
|
|
14
|
+
Requires-Dist: black~=26.5; extra == "format"
|
|
15
|
+
Provides-Extra: typecheck
|
|
16
|
+
Requires-Dist: mypy~=2.1; extra == "typecheck"
|
holoso-0.1.0/README.md
ADDED
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
<div align="center">
|
|
2
|
+
|
|
3
|
+
<img src="docs/holoso-logo-128.png" width="60px">
|
|
4
|
+
|
|
5
|
+
<h1>Holoso</h1>
|
|
6
|
+
|
|
7
|
+
_Simple high-level synthesis of portable Verilog from idiomatic Python_
|
|
8
|
+
|
|
9
|
+
[](https://holoso.digital/)
|
|
10
|
+
[](https://forum.zubax.com)
|
|
11
|
+
|
|
12
|
+
</div>
|
|
13
|
+
|
|
14
|
+
-----
|
|
15
|
+
|
|
16
|
+
Holoso converts a subset of idiomatic Python into synthesizable and verifiable Verilog.
|
|
17
|
+
It is primarily designed for heavy numerical code which is abundant in control systems and DSP
|
|
18
|
+
where manual RTL coding is inefficient and error-prone.
|
|
19
|
+
|
|
20
|
+
Holoso focuses on Python because this is a popular language in modeling, system design, and verification domains;
|
|
21
|
+
ability to generate production HDL directly from the original model allows the designer to work with much simpler
|
|
22
|
+
harnesses and iterate faster.
|
|
23
|
+
|
|
24
|
+
See [PRIOR_ART.md](PRIOR_ART.md) for a detailed review of existing alternatives,
|
|
25
|
+
and why none are good enough for practical use.
|
|
26
|
+
|
|
27
|
+
Holoso is under active development and as such it has no burden of backward compatibility.
|
|
28
|
+
Breaking changes will occur regularly without notice until v1.0 is out.
|
|
29
|
+
Many critical features are missing which may limit applicability beyond applications that we are immediately involved with.
|
|
30
|
+
Contributions of any kind are emphatically welcome!
|
|
31
|
+
|
|
32
|
+
<img src="docs/hero.png" width="900px">
|
|
33
|
+
|
|
34
|
+
## Design
|
|
35
|
+
|
|
36
|
+
Holoso implements essentially a separate programming language whose syntax is a strict subset of Python,
|
|
37
|
+
and whose semantics is largely equivalent to Python with minor deviations that make sense in chip design context.
|
|
38
|
+
Save for the minor differences in semantics, Holoso ensures that one can execute the original Python code
|
|
39
|
+
and run the generated circuit (RTL) side by side, and obtain equivalent results (bit-exact unless floating points
|
|
40
|
+
are used, in which case small errors may creep up, due to the inherent limitations of floating points).
|
|
41
|
+
|
|
42
|
+
Unlike most (all known to us) HLS engines out there, Holoso does not generate a straight-line II=1 pipeline
|
|
43
|
+
because this is rarely what you actually need in practice; instead, it designs a narrowly specialized
|
|
44
|
+
computing core (a zero-instruction-set processor) with custom microcode, and statically schedules a program for the
|
|
45
|
+
designed core. Being in control of both the core synthesis and the program compilation, Holoso tends to generate
|
|
46
|
+
extremely efficient designs in terms of cycle latency and chip area utilization compared to the state of the art.
|
|
47
|
+
|
|
48
|
+
Holoso outputs a purely portable and vendor-agnostic Verilog that can be fed into thid-party synthesis tools as-is,
|
|
49
|
+
along with its support library implementing various arithmetic operators. So far it has been tested at least with
|
|
50
|
+
Yosys (ECP5), Diamond (ECP5), and Vivado (Artix-7).
|
|
51
|
+
|
|
52
|
+
By default, Holoso is tuned for the minimum cycle latency and minimum $f_\max$.
|
|
53
|
+
If timing closure fails, one needs to locate the critical path and enable the staging knob that inserts a
|
|
54
|
+
register stage into the offending path; then re-synthesize and repeat until timings close.
|
|
55
|
+
|
|
56
|
+
Holoso has its own efficient floating point engine that is a subset of IEEE-754, omitting support for subnormals and NaN.
|
|
57
|
+
Arbitrary exponent and significand bit widths are supported (the IEEE-754 defaults map poorly onto FPGA DSP tiles).
|
|
58
|
+
|
|
59
|
+
Along with the synthesized Verilog, Holoso produces a Cocotb co-simulation testbench and a detailed and beautiful HTML
|
|
60
|
+
report that provides a human-friendly view of the processor and the microcode sequence constructed by the synthesizer.
|
|
61
|
+
|
|
62
|
+
>*You can SEE the pipeline — every cycle, every landing, every spill. It's gorgeous. People love it.*
|
|
63
|
+
>*They come up to me with tears in their eyes, they say sir, that schedule report, it's the most beautiful report we have ever seen.*
|
|
64
|
+
|
|
65
|
+
For a detailed review of the design and trade-offs, please refer to `DESIGN.md`.
|
|
66
|
+
|
|
67
|
+
## Usage
|
|
68
|
+
|
|
69
|
+
Unlike most tools in this domain, Holoso is trivial to set up and get started with; it is not a framework.
|
|
70
|
+
|
|
71
|
+
Install: `pip install holoso`.
|
|
72
|
+
|
|
73
|
+
```python
|
|
74
|
+
# Select the floating-point format you wish to use.
|
|
75
|
+
# Ideally, wman (mantissa width) should be a multiple of DSP tile operand width.
|
|
76
|
+
float_format = holoso.FloatFormat(wexp=6, wman=18)
|
|
77
|
+
|
|
78
|
+
# Define the numerical operators. This is where you can configure additional stages to close timings.
|
|
79
|
+
ops = holoso.OpConfig(
|
|
80
|
+
holoso.FAddOperator(float_format),
|
|
81
|
+
holoso.FMulOperator(float_format),
|
|
82
|
+
holoso.FDivOperator(float_format),
|
|
83
|
+
holoso.FMulILog2OperatorFamily(float_format),
|
|
84
|
+
holoso.FCmpOperator(float_format),
|
|
85
|
+
)
|
|
86
|
+
|
|
87
|
+
# Run Holoso -- construct the processor and the microcode.
|
|
88
|
+
# The results are returned in-memory; you can write them to disk where you want.
|
|
89
|
+
# They include the generated Verilog module, the fixed holoso_support.v/.vh, testbench, and the reports.
|
|
90
|
+
result = holoso.synthesize(your_function_or_method_here)
|
|
91
|
+
|
|
92
|
+
# Write the files -- this is usually what you want.
|
|
93
|
+
out = result.write(Path(__file__).resolve().parent)
|
|
94
|
+
|
|
95
|
+
# Show what's been written.
|
|
96
|
+
for filename, path in out.items():
|
|
97
|
+
print(f"{filename}: {path}")
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
See the `examples/` directory for self-contained usage examples.
|
|
101
|
+
|
|
102
|
+
## Verification
|
|
103
|
+
|
|
104
|
+
Just say `nox`. Read the `noxfile.py` and `DESIGN.md` for details.
|
|
105
|
+
|
|
106
|
+
You may find the [zubax-fpga-toolchain](https://github.com/Zubax/fpga-toolchain-docker/pkgs/container/zubax-fpga-toolchain)
|
|
107
|
+
container useful as it comes with all of the required tools out of the box.
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
"""Holoso: a narrow Python-to-Verilog synthesizer for numeric kernels."""
|
|
2
|
+
|
|
3
|
+
from ._api import synthesize as synthesize, SynthesisResult as SynthesisResult
|
|
4
|
+
from ._lir import (
|
|
5
|
+
ControlInputPort as ControlInputPort,
|
|
6
|
+
ControlOutputPort as ControlOutputPort,
|
|
7
|
+
ControlPort as ControlPort,
|
|
8
|
+
DataInputPort as DataInputPort,
|
|
9
|
+
DataOutputPort as DataOutputPort,
|
|
10
|
+
DataPort as DataPort,
|
|
11
|
+
Direction as Direction,
|
|
12
|
+
Port as Port,
|
|
13
|
+
)
|
|
14
|
+
from ._type import (
|
|
15
|
+
BoolType as BoolType,
|
|
16
|
+
FloatFormat as FloatFormat,
|
|
17
|
+
FloatType as FloatType,
|
|
18
|
+
LogicalPort as LogicalPort,
|
|
19
|
+
ScalarType as ScalarType,
|
|
20
|
+
)
|
|
21
|
+
from ._value import FloatValue as FloatValue
|
|
22
|
+
from ._errors import (
|
|
23
|
+
HolosoError as HolosoError,
|
|
24
|
+
MissingIntrinsic as MissingIntrinsic,
|
|
25
|
+
SourceUnavailable as SourceUnavailable,
|
|
26
|
+
SynthesisError as SynthesisError,
|
|
27
|
+
UnsupportedConstruct as UnsupportedConstruct,
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
from ._backend.cocotb import CocotbOutput as CocotbOutput
|
|
31
|
+
from ._backend.html import HtmlOutput as HtmlOutput
|
|
32
|
+
from ._backend.numerical import (
|
|
33
|
+
NumericalModel as NumericalModel,
|
|
34
|
+
NumericalSimulator as NumericalSimulator,
|
|
35
|
+
)
|
|
36
|
+
from ._backend.verilog import VerilogOutput as VerilogOutput
|
|
37
|
+
|
|
38
|
+
from ._operators import (
|
|
39
|
+
FAddOperator as FAddOperator,
|
|
40
|
+
FDivOperator as FDivOperator,
|
|
41
|
+
FMulILog2OperatorFamily as FMulILog2OperatorFamily,
|
|
42
|
+
FMulOperator as FMulOperator,
|
|
43
|
+
FCmpOperator as FCmpOperator,
|
|
44
|
+
OpConfig as OpConfig,
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
__version__ = "0.1.0"
|
|
48
|
+
__url__ = "https://github.com/Zubax/holoso"
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
"""The public synthesis entry point."""
|
|
2
|
+
|
|
3
|
+
from collections.abc import Callable
|
|
4
|
+
from typing import Any
|
|
5
|
+
from dataclasses import dataclass, fields
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
import inspect
|
|
8
|
+
import logging
|
|
9
|
+
import re
|
|
10
|
+
|
|
11
|
+
from ._backend.cocotb import generate as generate_testbench, CocotbOutput
|
|
12
|
+
from ._backend.html import generate as generate_html, HtmlOutput
|
|
13
|
+
from ._backend.numerical import generate as generate_model, NumericalModel
|
|
14
|
+
from ._backend.verilog import generate as generate_verilog, VerilogOutput
|
|
15
|
+
|
|
16
|
+
from ._frontend import lower as lower_frontend
|
|
17
|
+
from ._hir import optimize
|
|
18
|
+
from ._lir import ControlPort, DataInputPort, DataOutputPort, Port, build
|
|
19
|
+
from ._mir import lower as lower_to_mir
|
|
20
|
+
from ._operators import OpConfig
|
|
21
|
+
|
|
22
|
+
type Target = Callable[..., Any]
|
|
23
|
+
"""
|
|
24
|
+
Currently supported targets are:
|
|
25
|
+
- A plain stateless function or lambda.
|
|
26
|
+
- A bound method of a class instance -- stateful. Public attributes become additional output ports.
|
|
27
|
+
- Later on we may potentially add support for multiple methods per class, where the generated module will provide
|
|
28
|
+
a selector port to choose which method to execute, all sharing the same state. In this case we would accept
|
|
29
|
+
a tuple containing the class type and a list of its unbound methods. This remains to be seen.
|
|
30
|
+
"""
|
|
31
|
+
|
|
32
|
+
_logger = logging.getLogger(__name__)
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
@dataclass(frozen=True, slots=True)
|
|
36
|
+
class SynthesisResult:
|
|
37
|
+
"""Everything produced by a synthesis run, held in memory. Nothing is written to disk unless requested."""
|
|
38
|
+
|
|
39
|
+
module_name: str
|
|
40
|
+
|
|
41
|
+
ports: list[Port]
|
|
42
|
+
input_ports: list[DataInputPort]
|
|
43
|
+
output_ports: list[DataOutputPort]
|
|
44
|
+
control_ports: list[ControlPort]
|
|
45
|
+
|
|
46
|
+
initiation_interval: tuple[int, int | None] # (min II, max II or None if data-dependent)
|
|
47
|
+
verilog_output: VerilogOutput
|
|
48
|
+
numerical_model: NumericalModel
|
|
49
|
+
cocotb_output: CocotbOutput
|
|
50
|
+
html_output: HtmlOutput
|
|
51
|
+
|
|
52
|
+
def write(self, out_dir: Path | str) -> dict[str, Path]:
|
|
53
|
+
"""
|
|
54
|
+
Write every artifact to ``out_dir`` and return the written paths keyed by filename.
|
|
55
|
+
This is the only Holoso operation that touches the filesystem.
|
|
56
|
+
"""
|
|
57
|
+
directory = Path(out_dir)
|
|
58
|
+
directory.mkdir(parents=True, exist_ok=True)
|
|
59
|
+
files: dict[str, str] = {
|
|
60
|
+
f"{self.module_name}.v": self.verilog_output.verilog,
|
|
61
|
+
**self.verilog_output.support_files,
|
|
62
|
+
f"test_{self.module_name}.py": self.cocotb_output.testbench,
|
|
63
|
+
f"{self.module_name}.html": self.html_output.html,
|
|
64
|
+
}
|
|
65
|
+
written: dict[str, Path] = {}
|
|
66
|
+
for filename, content in files.items():
|
|
67
|
+
path = directory / filename
|
|
68
|
+
path.write_text(content, encoding="utf-8")
|
|
69
|
+
written[filename] = path
|
|
70
|
+
return written
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def synthesize(target: Target, /, ops: OpConfig, *, name: str | None = None) -> SynthesisResult:
|
|
74
|
+
"""
|
|
75
|
+
Synthesize ``target`` (a plain function or a bound method of a constructed instance) into RTL.
|
|
76
|
+
``ops`` is the operator configuration, constructed explicitly by the caller: each field fixes one operator's
|
|
77
|
+
float format and parameters, including any pipeline-stage knobs that lengthen its latency to ease timing closure.
|
|
78
|
+
``name`` overrides the generated module name (inferred from target by default).
|
|
79
|
+
"""
|
|
80
|
+
logging.basicConfig(level=logging.INFO, format="%(levelname)-5.5s %(name)s: %(message)s") # no-op if already setup
|
|
81
|
+
module_name: str = name or _default_module_name(target)
|
|
82
|
+
_validate_module_name(module_name)
|
|
83
|
+
_logger.info("Synthesis start: module=%r target=%r", module_name, target)
|
|
84
|
+
_logger.info("Configured operators:")
|
|
85
|
+
for field in fields(ops):
|
|
86
|
+
_logger.info("\t%s: %s", field.name, getattr(ops, field.name))
|
|
87
|
+
|
|
88
|
+
hir = optimize(lower_frontend(target))
|
|
89
|
+
_logger.info("HIR:\n\tinputs=%s\n\toutputs=%s\n\thir_nodes=%d", hir.input_ids, hir.outputs, len(hir.nodes))
|
|
90
|
+
|
|
91
|
+
mir = lower_to_mir(hir, ops)
|
|
92
|
+
|
|
93
|
+
lir = build(mir, module_name)
|
|
94
|
+
_logger.info("LIR ports:\n\t%s", "\n\t".join(f"{port}" for port in lir.ports))
|
|
95
|
+
|
|
96
|
+
verilog_output = generate_verilog(lir)
|
|
97
|
+
html_output = generate_html(lir, verilog_output)
|
|
98
|
+
model = generate_model(lir)
|
|
99
|
+
cocotb_output = generate_testbench(model)
|
|
100
|
+
|
|
101
|
+
latency_is_exact = len(lir.blocks) == 1 # a straight-line kernel has one fixed path; branches/loops vary by data
|
|
102
|
+
ii = (lir.min_initiation_interval, lir.min_initiation_interval if latency_is_exact else None)
|
|
103
|
+
_logger.info("Generated Verilog: %s; II [min,max]: %s cycles", verilog_output, ii)
|
|
104
|
+
return SynthesisResult(
|
|
105
|
+
module_name=module_name,
|
|
106
|
+
ports=lir.ports,
|
|
107
|
+
input_ports=lir.input_ports,
|
|
108
|
+
output_ports=lir.output_ports,
|
|
109
|
+
control_ports=lir.control_ports,
|
|
110
|
+
initiation_interval=ii,
|
|
111
|
+
verilog_output=verilog_output,
|
|
112
|
+
numerical_model=model,
|
|
113
|
+
cocotb_output=cocotb_output,
|
|
114
|
+
html_output=html_output,
|
|
115
|
+
)
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def _default_module_name(target: Target) -> str:
|
|
119
|
+
if inspect.ismethod(target):
|
|
120
|
+
n = type(target.__self__).__name__
|
|
121
|
+
if "__" not in target.__name__:
|
|
122
|
+
n += f"_{target.__name__}"
|
|
123
|
+
return n
|
|
124
|
+
return str(getattr(target, "__name__", "kernel"))
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def _validate_module_name(name: str) -> None:
|
|
128
|
+
if _MODULE_NAME.fullmatch(name) is None:
|
|
129
|
+
raise ValueError(f"module name {name!r} is not a valid identifier; expected [A-Za-z_][A-Za-z0-9_]*")
|
|
130
|
+
if name in _BLACKLIST:
|
|
131
|
+
raise ValueError(f"module name {name!r} is a reserved keyword; choose another name")
|
|
132
|
+
if name.lower().startswith("holoso"):
|
|
133
|
+
raise ValueError(f"module name {name!r} uses the reserved 'holoso' prefix; choose another name")
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
_MODULE_NAME = re.compile(r"[A-Za-z_][A-Za-z0-9_]*")
|
|
137
|
+
|
|
138
|
+
# Keywords from supported HDLs etc. that are not valid module names. Includes Verilog and VHDL keywords.
|
|
139
|
+
_BLACKLIST = frozenset("""
|
|
140
|
+
always and assign automatic begin buf bufif0 bufif1 case casex casez cell cmos config deassign default defparam
|
|
141
|
+
design disable edge else end endcase endconfig endfunction endgenerate endmodule endprimitive endspecify endtable
|
|
142
|
+
endtask event for force forever fork function generate genvar highz0 highz1 if ifnone incdir include initial inout
|
|
143
|
+
input instance integer join large liblist library localparam macromodule medium module nand negedge nmos nor
|
|
144
|
+
noshowcancelled not notif0 notif1 or output parameter pmos posedge primitive pull0 pull1 pulldown pullup
|
|
145
|
+
pulsestyle_onevent pulsestyle_ondetect rcmos real realtime reg release repeat rnmos rpmos rtran rtranif0 rtranif1
|
|
146
|
+
scalared showcancelled signed small specify specparam strong0 strong1 supply0 supply1 table task time tran tranif0
|
|
147
|
+
tranif1 tri tri0 tri1 triand trior trireg unsigned use uwire vectored wait wand weak0 weak1 while wire wor xnor xor
|
|
148
|
+
abs access after alias all architecture array assert attribute block body buffer bus component configuration constant
|
|
149
|
+
context disconnect downto elsif entity exit file generic group guarded impure in inertial is label linkage literal
|
|
150
|
+
loop map mod new next null of on open others out package port postponed procedure process protected pure
|
|
151
|
+
range record register reject rem report return rol ror select severity signal shared sla sll sra srl subtype
|
|
152
|
+
then to transport type unaffected units until variable when with
|
|
153
|
+
accept_on always_comb always_ff always_latch assume before bind bins binsof bit break byte chandle checker
|
|
154
|
+
class clocking const constraint continue cover covergroup coverpoint cross dist do endchecker endclass
|
|
155
|
+
endclocking endgroup endinterface endpackage endprogram endproperty endsequence enum expect export extends extern
|
|
156
|
+
final first_match foreach forkjoin global iff ignore_bins illegal_bins implements implies import inside int interface
|
|
157
|
+
intersect join_any join_none let logic longint matches modport nettype packed priority program property rand randc
|
|
158
|
+
randcase randsequence ref reject_on restrict s_always s_eventually s_nexttime s_until s_until_with sequence shortint
|
|
159
|
+
shortreal soft solve static string strong struct super sync_accept_on sync_reject_on tagged this throughout
|
|
160
|
+
timeprecision timeunit typedef union unique unique0 var virtual void wait_order weak wildcard within
|
|
161
|
+
assume_guarantee eventually fairness interconnect local nexttime restrict_guarantee untyped until_with vmode vprop vunit
|
|
162
|
+
false none true as async await def del elif except finally from lambda nonlocal pass raise try yield
|
|
163
|
+
""".split())
|
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
"""
|
|
2
|
+
The cocotb backend: a self-contained, self-checking testbench for a generated module.
|
|
3
|
+
|
|
4
|
+
``generate`` embeds the module's :class:`NumericalModel` handle into a standalone cocotb test. The bench unpickles the
|
|
5
|
+
handle, elaborates a :class:`NumericalSimulator`, and ticks it in cycle-by-cycle lockstep with the DUT: each clock it
|
|
6
|
+
asserts that ``out_valid``/``in_ready`` agree (so the model reaches ``out_valid`` on exactly the DUT's cycle -- the
|
|
7
|
+
data-dependent latency check) and that the output bits match when valid, back-pressure included. The model is bit-exact
|
|
8
|
+
to the RTL, so the output check needs no tolerance.
|
|
9
|
+
|
|
10
|
+
The input sequence is either an explicit list of vectors supplied by the caller (replayed verbatim) or, when none is
|
|
11
|
+
given, a default random sweep drawn from a fixed seed. The seed also drives the back-pressure stalls, so every generated
|
|
12
|
+
bench is fully reproducible.
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
import base64
|
|
16
|
+
import pickle
|
|
17
|
+
import zlib
|
|
18
|
+
from collections.abc import Mapping
|
|
19
|
+
from dataclasses import dataclass
|
|
20
|
+
|
|
21
|
+
from .numerical import NumericalModel
|
|
22
|
+
|
|
23
|
+
# language=python
|
|
24
|
+
_TEMPLATE = '''\
|
|
25
|
+
"""
|
|
26
|
+
Auto-generated by Holoso: bit-exact cocotb testbench for module @@MODULE@@.
|
|
27
|
+
"""
|
|
28
|
+
|
|
29
|
+
import base64
|
|
30
|
+
import pickle
|
|
31
|
+
import random
|
|
32
|
+
import zlib
|
|
33
|
+
|
|
34
|
+
import cocotb
|
|
35
|
+
from cocotb.clock import Clock
|
|
36
|
+
from cocotb.triggers import FallingEdge, RisingEdge, Timer
|
|
37
|
+
|
|
38
|
+
# TODO: make it self-contained by removing the holoso dependency: https://github.com/Zubax/holoso/issues/12
|
|
39
|
+
# Currently it is needed for unpickling.
|
|
40
|
+
import holoso
|
|
41
|
+
|
|
42
|
+
# Unpickle the opaque model handle and elaborate the runnable simulator the bench drives in lockstep with the DUT.
|
|
43
|
+
_SIM = pickle.loads(zlib.decompress(base64.b64decode("@@BLOB@@"))).elaborate() # TODO issues/12
|
|
44
|
+
_IN_PORTS = _SIM.inputs # logical input ports (name + scalar type), in module-port order
|
|
45
|
+
_OUT_PORTS = _SIM.outputs
|
|
46
|
+
|
|
47
|
+
# An explicit input sequence -- rows of per-input ZKF bits ordered as the input ports -- or None to draw the default
|
|
48
|
+
# random sweep below. The fixed seed makes the default sweep and the back-pressure stalls reproducible.
|
|
49
|
+
_VECTORS = @@VECTORS@@
|
|
50
|
+
_SEED = 0x9E3779B97F4A7C15 # TODO: allow overriding the seed, count, and range via plusargs.
|
|
51
|
+
_DEFAULT_COUNT = 64
|
|
52
|
+
_DEFAULT_RANGE = (-4.0, +4.0) # small bounded range keeps multi-operation kernels from overflowing into infinities
|
|
53
|
+
_MAX_TRANSACTION_CYCLES = 1 << 20 # out_valid ceiling: well above any real transaction, tripped only by a runaway loop
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def _output_bits(value):
|
|
57
|
+
return int(value) if isinstance(value, bool) else value.bits
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def _input_bits(port, rng, lo, hi):
|
|
61
|
+
# Encode one random input per its scalar type: a boolean draws a single bit, a float a ZKF-encoded sample.
|
|
62
|
+
match port.scalar_type:
|
|
63
|
+
case holoso.BoolType():
|
|
64
|
+
return rng.randint(0, 1)
|
|
65
|
+
case holoso.FloatType(fmt=fmt):
|
|
66
|
+
return fmt.encode(rng.uniform(lo, hi))
|
|
67
|
+
case other:
|
|
68
|
+
raise TypeError("unsupported input scalar type: %r" % (other,))
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def _input_value(port, bits):
|
|
72
|
+
match port.scalar_type:
|
|
73
|
+
case holoso.BoolType():
|
|
74
|
+
return bool(bits)
|
|
75
|
+
case holoso.FloatType(fmt=fmt):
|
|
76
|
+
return holoso.FloatValue.from_bits(fmt, bits)
|
|
77
|
+
case other:
|
|
78
|
+
raise TypeError("unsupported input scalar type: %r" % (other,))
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
@cocotb.test()
|
|
82
|
+
async def cosim(dut):
|
|
83
|
+
rng = random.Random(_SEED)
|
|
84
|
+
if _VECTORS is not None:
|
|
85
|
+
sequence = _VECTORS
|
|
86
|
+
else:
|
|
87
|
+
lo, hi = _DEFAULT_RANGE
|
|
88
|
+
sequence = [[_input_bits(p, rng, lo, hi) for p in _IN_PORTS] for _ in range(_DEFAULT_COUNT)]
|
|
89
|
+
|
|
90
|
+
cocotb.start_soon(Clock(dut.clk, 10, unit="ns").start())
|
|
91
|
+
await FallingEdge(dut.clk)
|
|
92
|
+
dut.rst.value = 1
|
|
93
|
+
dut.in_valid.value = 0
|
|
94
|
+
dut.out_ready.value = 0
|
|
95
|
+
for _ in range(4):
|
|
96
|
+
await RisingEdge(dut.clk)
|
|
97
|
+
dut.rst.value = 0
|
|
98
|
+
await FallingEdge(dut.clk)
|
|
99
|
+
_SIM.reset() # the simulator and the DUT are now both idle at pc==0 (in_ready), aligned cycle for cycle from here
|
|
100
|
+
|
|
101
|
+
async def lockstep(in_valid, out_ready):
|
|
102
|
+
# Advance ONE clock on the DUT and the simulator with the identical handshake, then assert they still agree. The
|
|
103
|
+
# simulator is the cycle-accurate reference, so out_valid asserting on the same cycle is the latency check, and
|
|
104
|
+
# in_ready agreeing keeps the accept handshake aligned.
|
|
105
|
+
dut.in_valid.value = 1 if in_valid else 0
|
|
106
|
+
dut.out_ready.value = 1 if out_ready else 0
|
|
107
|
+
await RisingEdge(dut.clk)
|
|
108
|
+
await Timer(1, unit="ns")
|
|
109
|
+
_SIM.tick(in_valid, out_ready)
|
|
110
|
+
assert _SIM.out_valid == bool(int(dut.out_valid.value)), "out_valid diverged: model %d dut %d" % (
|
|
111
|
+
int(_SIM.out_valid),
|
|
112
|
+
int(dut.out_valid.value),
|
|
113
|
+
)
|
|
114
|
+
assert _SIM.in_ready == bool(int(dut.in_ready.value)), "in_ready diverged: model %d dut %d" % (
|
|
115
|
+
int(_SIM.in_ready),
|
|
116
|
+
int(dut.in_ready.value),
|
|
117
|
+
)
|
|
118
|
+
|
|
119
|
+
def check_outputs(index):
|
|
120
|
+
for port, value in zip(_OUT_PORTS, _SIM.output_values):
|
|
121
|
+
want = _output_bits(value)
|
|
122
|
+
got = int(getattr(dut, port.name).value)
|
|
123
|
+
assert got == want, "vector %d port %s: got 0x%x expected 0x%x" % (index, port.name, got, want)
|
|
124
|
+
|
|
125
|
+
for index, in_bits in enumerate(sequence):
|
|
126
|
+
waited = 0
|
|
127
|
+
while not _SIM.in_ready: # idle until both can accept; they advance together so they reach it together
|
|
128
|
+
await lockstep(False, False)
|
|
129
|
+
waited += 1
|
|
130
|
+
assert waited < _MAX_TRANSACTION_CYCLES, "vector %d: in_ready not reached" % index
|
|
131
|
+
# Accept: present the same input bits to the DUT and the simulator, then pulse in_valid for one cycle.
|
|
132
|
+
for port, bits in zip(_IN_PORTS, in_bits):
|
|
133
|
+
getattr(dut, "in_" + port.name).value = int(bits)
|
|
134
|
+
_SIM.set_inputs(*[_input_value(p, bits) for p, bits in zip(_IN_PORTS, in_bits)])
|
|
135
|
+
await lockstep(True, False) # accept edge: pc 0 -> 1
|
|
136
|
+
# Run to out_valid in lockstep. The simulator reaches it on the same cycle as the DUT (the lockstep asserts so
|
|
137
|
+
# each cycle), which is the cycle-accurate, data-dependent latency check -- no separate prediction needed.
|
|
138
|
+
waited = 0
|
|
139
|
+
while not _SIM.out_valid:
|
|
140
|
+
await lockstep(False, False)
|
|
141
|
+
waited += 1
|
|
142
|
+
assert waited < _MAX_TRANSACTION_CYCLES, "vector %d: out_valid not reached within %d cycles" % (
|
|
143
|
+
index,
|
|
144
|
+
_MAX_TRANSACTION_CYCLES,
|
|
145
|
+
)
|
|
146
|
+
assert int(dut.err_pc.value) == 0, "vector %d: unexpected error at cycle %d" % (index, int(dut.err_pc.value))
|
|
147
|
+
check_outputs(index)
|
|
148
|
+
# Random back-pressure: hold out_ready low. out_valid and the outputs must stay stable on both, and neither
|
|
149
|
+
# advances its persistent state until the transaction is accepted (out_valid && out_ready).
|
|
150
|
+
for _ in range(rng.randint(0, 3)):
|
|
151
|
+
assert _SIM.out_valid, "vector %d: simulator dropped out_valid under back-pressure" % index
|
|
152
|
+
check_outputs(index)
|
|
153
|
+
await lockstep(False, False)
|
|
154
|
+
check_outputs(index)
|
|
155
|
+
await lockstep(False, True) # accept the output: both advance the persistent state, pc -> 0
|
|
156
|
+
'''
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
@dataclass(frozen=True, slots=True)
|
|
160
|
+
class CocotbOutput:
|
|
161
|
+
"""The cocotb backend's output: one self-contained, self-checking cocotb testbench module."""
|
|
162
|
+
|
|
163
|
+
testbench: str
|
|
164
|
+
|
|
165
|
+
def __str__(self) -> str:
|
|
166
|
+
return f"{type(self).__name__}(testbench_bytes={len(self.testbench.encode())})"
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
def _embed_vectors(input_names: list[str], vectors: list[Mapping[str, int]] | None) -> str:
|
|
170
|
+
"""Render the explicit input sequence as a Python literal of input-name-ordered ZKF-bit rows, or ``None``."""
|
|
171
|
+
if vectors is None:
|
|
172
|
+
return "None"
|
|
173
|
+
rows: list[list[int]] = []
|
|
174
|
+
for index, vector in enumerate(vectors):
|
|
175
|
+
if missing := [name for name in input_names if name not in vector]:
|
|
176
|
+
raise ValueError(f"cosim vector {index} is missing inputs {missing}")
|
|
177
|
+
rows.append([int(vector[name]) for name in input_names])
|
|
178
|
+
return repr(rows)
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
def generate(model: NumericalModel, vectors: list[Mapping[str, int]] | None = None) -> CocotbOutput:
|
|
182
|
+
"""
|
|
183
|
+
Build a self-contained cocotb testbench that checks the DUT against the embedded bit-exact model.
|
|
184
|
+
|
|
185
|
+
When ``vectors`` is given, each maps an input-port name to its ZKF bits and the bench replays the sequence verbatim
|
|
186
|
+
(one accepted transaction per vector, state carried across for stateful modules). When it is ``None``, the bench
|
|
187
|
+
draws a fixed-seed random sweep over a small bounded range instead. Either way the run is fully reproducible.
|
|
188
|
+
"""
|
|
189
|
+
blob = base64.b64encode(zlib.compress(pickle.dumps(model, pickle.HIGHEST_PROTOCOL))).decode("ascii")
|
|
190
|
+
testbench = (
|
|
191
|
+
_TEMPLATE.replace("@@MODULE@@", model.module_name)
|
|
192
|
+
.replace("@@VECTORS@@", _embed_vectors([port.name for port in model.inputs], vectors))
|
|
193
|
+
.replace("@@BLOB@@", blob)
|
|
194
|
+
)
|
|
195
|
+
return CocotbOutput(testbench=testbench)
|