Skip to content

Commit 88c1baf

Browse files
committed
Codegen to Testbench C++
Given the PDA-QP parameters `PDAQPAlgoConfig`, generate the reference implementation in C++20: `constants.hpp` and `problem-def.hpp`. Also, given the hardware accelerator config `PDAQPHWConfig`, generate the corresponding config file in Chisel HDL language `Constants.scala`. The hardware accelerator project code will be released soon.
1 parent 2f0126a commit 88c1baf

10 files changed

Lines changed: 352 additions & 10 deletions

File tree

cvxpygen/hwgen/codegen.py

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
from pathlib import Path
2+
3+
from jinja2 import Environment, PackageLoader, select_autoescape
4+
from numpy import concatenate, hstack, newaxis
5+
6+
from cvxpygen.hwgen.firmware_models import (
7+
ExecutionEnvironment,
8+
)
9+
from cvxpygen.hwgen.hw_models import PDAQPHWConfig
10+
from cvxpygen.hwgen.models import PDAQPAlgoConfig
11+
12+
env = Environment(
13+
loader=PackageLoader("cvxpygen.hwgen"), autoescape=select_autoescape()
14+
)
15+
16+
17+
def generateHWAccCode(config: PDAQPHWConfig, target_dir: Path) -> Path:
18+
"""Given the high-level hardware accelerator design for
19+
PDA-QP solver, generate the design in the Chisel language."""
20+
21+
template = env.get_template("hwacc/Constants.scala.j2")
22+
rendered = template.render(config=config)
23+
24+
outfile_path = target_dir / "Constants.scala"
25+
with open(outfile_path, "w") as f:
26+
f.write(rendered)
27+
28+
return outfile_path
29+
30+
31+
def generateTestbenchCode(
32+
config: PDAQPAlgoConfig,
33+
fixed_point_precision: int,
34+
execution_environment: ExecutionEnvironment,
35+
target_dir: Path,
36+
) -> tuple[Path, Path]:
37+
"""Given the high-level algorithm design for
38+
PDA-QP solver, generate the design in the Chisel language."""
39+
40+
constants_hpp_path = target_dir / "constants.hpp"
41+
with open(constants_hpp_path, "w") as f:
42+
rendered = env.get_template("testbench/constants.hpp.j2").render(
43+
problem_size=config.problem_size,
44+
Q=fixed_point_precision,
45+
)
46+
f.write(rendered)
47+
48+
problem_def_hpp_path = target_dir / "problem-def.hpp"
49+
with open(problem_def_hpp_path, "w") as f:
50+
# TODO (antonysigma): The shape of the halfplanes normal and
51+
# offset are erased. Modify the C++ project to preserve the shape.
52+
53+
assert config.feedbacks.scale.shape[0] == config.feedbacks.offset.shape[0]
54+
assert config.feedbacks.scale.shape[1] == config.feedbacks.offset.shape[1]
55+
assert config.feedbacks.scale.shape[2] == config.problem_size.n_parameter
56+
57+
rendered = env.get_template("testbench/problem-def.hpp.j2").render(
58+
half_planes=hstack(
59+
(config.half_planes.scale, config.half_planes.offset[:, newaxis])
60+
),
61+
feedbacks=concatenate(
62+
(config.feedbacks.scale, config.feedbacks.offset[..., newaxis]),
63+
axis=-1,
64+
),
65+
tree_nodes=config.tree_nodes,
66+
)
67+
f.write(rendered)
68+
69+
return constants_hpp_path, problem_def_hpp_path

cvxpygen/hwgen/decode.py

Lines changed: 17 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
from cvxpygen.hwgen.c_parsing.array_visitor import FloatArrayVisitor, IntArrayVisitor
66
from cvxpygen.hwgen.c_parsing.grammars import pdaqp_c_grammar, pdaqp_header_grammar
77
from cvxpygen.hwgen.c_parsing.header_visitor import HeaderVisitor
8-
from cvxpygen.hwgen.models import MultiplyAddFP32, PDAQPAlgoConfig
8+
from cvxpygen.hwgen.models import BinaryTree, MultiplyAddFP32, PDAQPAlgoConfig
99

1010

1111
def decode(c_path: Path, h_path: Path) -> PDAQPAlgoConfig:
@@ -41,19 +41,32 @@ def decode(c_path: Path, h_path: Path) -> PDAQPAlgoConfig:
4141
== 0
4242
)
4343
feedbacks = float_arrays.arrays["pdaqp_feedbacks"].data.reshape(
44-
-1, n_parameter + 1, n_solution
44+
-1, n_solution, n_parameter + 1
4545
)
4646

4747
assert "pdaqp_hp_list" in int_arrays.arrays
4848
assert int_arrays.arrays["pdaqp_hp_list"].data.max() <= 65535
4949

50+
assert "pdaqp_jump_list" in int_arrays.arrays
51+
assert int_arrays.arrays["pdaqp_jump_list"].data.max() <= 65535
52+
53+
assert (
54+
int_arrays.arrays["pdaqp_hp_list"].data.size
55+
== int_arrays.arrays["pdaqp_jump_list"].data.size
56+
)
57+
5058
return PDAQPAlgoConfig(
5159
header_visitor.problem_size,
52-
tree_nodes=int_arrays.arrays["pdaqp_hp_list"].data.astype(uint16),
60+
tree_nodes=BinaryTree(
61+
halfplane_or_feedback_id=int_arrays.arrays["pdaqp_hp_list"].data.astype(
62+
uint16
63+
),
64+
jump=int_arrays.arrays["pdaqp_jump_list"].data.astype(uint16),
65+
),
5366
half_planes=MultiplyAddFP32(
5467
halfplanes[:, :n_parameter], halfplanes[:, n_parameter]
5568
),
5669
feedbacks=MultiplyAddFP32(
57-
feedbacks[:, :n_parameter], feedbacks[:, n_parameter]
70+
feedbacks[..., :n_parameter], feedbacks[..., n_parameter]
5871
),
5972
)

cvxpygen/hwgen/firmware_models.py

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
from dataclasses import dataclass
2+
from enum import Enum
3+
4+
5+
class OperatingSystem(Enum):
6+
Linux = 0
7+
NoOS = 1
8+
9+
10+
class HardwareCapability(Enum):
11+
BranchPrediction = 0
12+
SIMDInstructions = 1
13+
InstructionCache = 2
14+
15+
16+
@dataclass
17+
class ExecutionEnvironment:
18+
os: OperatingSystem
19+
cpu_bits: int
20+
hardware_capabilities: list[HardwareCapability]
21+
22+
23+
LINUX_AMD64 = ExecutionEnvironment(
24+
os=OperatingSystem.Linux,
25+
cpu_bits=64,
26+
hardware_capabilities=[
27+
HardwareCapability.SIMDInstructions,
28+
HardwareCapability.BranchPrediction,
29+
HardwareCapability.InstructionCache,
30+
],
31+
)
32+
33+
ARM64 = ExecutionEnvironment(
34+
os=OperatingSystem.NoOS,
35+
cpu_bits=32,
36+
hardware_capabilities=[
37+
HardwareCapability.SIMDInstructions,
38+
HardwareCapability.BranchPrediction,
39+
],
40+
)
41+
42+
ATMETA8 = ExecutionEnvironment(
43+
os=OperatingSystem.NoOS,
44+
cpu_bits=8,
45+
hardware_capabilities=[],
46+
)

cvxpygen/hwgen/hw_models.py

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,18 @@
11
from dataclasses import dataclass
22

3-
from numpy import uint16
4-
from numpy.typing import NDArray
5-
6-
from cvxpygen.hwgen.models import MultiplyAddFix16, MultiplyAddFP32, ProblemSize
3+
from cvxpygen.hwgen.models import (
4+
BinaryTree,
5+
MultiplyAddFix16,
6+
MultiplyAddFP32,
7+
ProblemSize,
8+
)
79

810

911
@dataclass
1012
class TreeWalkerFSM:
1113
"""A tree walking module implemented as finite state machine."""
1214

13-
tree_nodes: NDArray[uint16]
15+
tree_nodes: BinaryTree
1416

1517

1618
@dataclass

cvxpygen/hwgen/models.py

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,10 +41,20 @@ class MultiplyAddFix16:
4141
Q: int = 14
4242

4343

44+
@dataclass
45+
class BinaryTree:
46+
halfplane_or_feedback_id: NDArray[uint16]
47+
jump: NDArray[uint16]
48+
49+
@property
50+
def size(self) -> int:
51+
return self.jump.size
52+
53+
4454
@dataclass
4555
class PDAQPAlgoConfig:
4656
problem_size: ProblemSize
47-
tree_nodes: NDArray[uint16]
57+
tree_nodes: BinaryTree
4858
half_planes: MultiplyAddFP32
4959
feedbacks: MultiplyAddFP32
5060

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
import scala.util.Random
2+
import chisel3._
3+
import chisel3.util._
4+
5+
object Constants {
6+
val rnd = new Random()
7+
8+
// BEGIN compile-time constants (already quantized)
9+
final val n_solutions = {{ config.problem_size.n_solution }}
10+
final val n_params = {{ config.problem_size.n_parameter }}
11+
final val n_nodes = {{ config.n_tree_nodes }}
12+
final val n_feedbacks = {{ config.n_feedbacks }}
13+
final val Q = {{ config.feedback_module.param.Q }}
14+
15+
final val hyperplane_normal = Seq(
16+
{% for item in config.half_planes_module.param.scale.ravel() -%}
17+
{{ item }},
18+
{% endfor -%}
19+
)
20+
21+
final val hyperplane_offset = Seq(
22+
{% for item in config.half_planes_module.param.offset.ravel() -%}
23+
{{ item }},
24+
{% endfor -%}
25+
)
26+
27+
final val feedbackA = Seq(
28+
{% for item in config.feedback_module.param.scale.ravel() -%}
29+
{{ item }},
30+
{% endfor -%}
31+
)
32+
33+
final val feedbackC = Seq(
34+
{% for item in config.feedback_module.param.offset.ravel() -%}
35+
{{ item }},
36+
{% endfor -%}
37+
)
38+
39+
final val hpList = Seq(
40+
{% for item in config.tree_walker.tree_nodes.halfplane_or_feedback_id -%}
41+
{{ item }},
42+
{% endfor -%}
43+
)
44+
45+
final val jumpList = Seq(
46+
{% for item in config.tree_walker.tree_nodes.jump -%}
47+
{{ item }},
48+
{% endfor -%}
49+
)
50+
// END compile-time constants (already quantized)
51+
}
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
#pragma once
2+
#include <cstdint>
3+
4+
#include "fixed_math.hpp"
5+
#include "log2ceil.hpp"
6+
7+
constexpr uint16_t n_parameter = {{ problem_size.n_parameter }};
8+
constexpr uint16_t n_solution = {{ problem_size.n_solution }};
9+
10+
using DataFormat = math::fixed<int16_t, {{ Q }}>;
11+
using AccuDataFormat = math::fixed<int32_t, {{ Q }} * 2 - log2ceil(n_parameter) - 1>;
12+
13+
static_assert(DataFormat::_Q <= AccuDataFormat::_Q, "Insufficient precision for the numerical product");
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
#pragma once
2+
#include <array>
3+
#include <cstdint>
4+
5+
constexpr std::array<float, {{ half_planes.size }}> pdaqp_halfplanes{
6+
{% for item in half_planes.ravel() -%}
7+
{{ item }}f,
8+
{% endfor -%}
9+
};
10+
constexpr std::array<float, {{ feedbacks.size }}> pdaqp_feedbacks = {
11+
{% for item in feedbacks.ravel() -%}
12+
{{ item }}f,
13+
{% endfor -%}
14+
};
15+
16+
constexpr std::array<uint16_t, {{ tree_nodes.halfplane_or_feedback_id.size }}> pdaqp_hp_list{
17+
{% for item in tree_nodes.halfplane_or_feedback_id.ravel() -%}
18+
{{ item }},
19+
{% endfor -%}
20+
};
21+
constexpr std::array<uint16_t, {{ tree_nodes.jump.size }}> pdaqp_jump_list{
22+
{% for item in tree_nodes.jump.ravel() -%}
23+
{{ item }},
24+
{% endfor -%}
25+
};
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
from pathlib import Path
2+
3+
from numpy import float32, uint16
4+
from numpy.random import randint, randn
5+
from numpy.typing import NDArray
6+
7+
from cvxpygen.hwgen.codegen import generateTestbenchCode
8+
from cvxpygen.hwgen.firmware_models import LINUX_AMD64
9+
from cvxpygen.hwgen.models import (
10+
BinaryTree,
11+
MultiplyAddFP32,
12+
PDAQPAlgoConfig,
13+
ProblemSize,
14+
)
15+
16+
17+
def test_codegen() -> None:
18+
problem_size = ProblemSize(2, 3)
19+
n_tree_nodes = 5
20+
n_feedbacks = 3
21+
22+
def generate_rand(size: tuple) -> NDArray[float32]:
23+
vmax = 20_000
24+
return randn(*size).astype(float32)
25+
26+
config = PDAQPAlgoConfig(
27+
problem_size,
28+
tree_nodes=BinaryTree(
29+
halfplane_or_feedback_id=randint(0, n_tree_nodes, size=n_tree_nodes).astype(
30+
uint16
31+
),
32+
jump=randint(0, n_tree_nodes, size=n_tree_nodes).astype(uint16),
33+
),
34+
half_planes=MultiplyAddFP32(
35+
generate_rand(
36+
(n_tree_nodes - n_feedbacks, problem_size.n_parameter),
37+
),
38+
generate_rand((n_tree_nodes - n_feedbacks,)),
39+
),
40+
feedbacks=MultiplyAddFP32(
41+
generate_rand(
42+
(n_feedbacks, problem_size.n_solution, problem_size.n_parameter),
43+
),
44+
generate_rand(
45+
(
46+
n_feedbacks,
47+
problem_size.n_solution,
48+
),
49+
),
50+
),
51+
)
52+
53+
fixed_point_precision = 14
54+
constants_hpp, problem_def_hpp = generateTestbenchCode(
55+
config, fixed_point_precision, LINUX_AMD64, Path("/tmp")
56+
)
57+
assert constants_hpp.exists()
58+
assert problem_def_hpp.exists()

0 commit comments

Comments
 (0)