Skip to content

Commit 7b94a53

Browse files
authored
Implment basic ONNX serialisation for models (#226)
1 parent b3d0be2 commit 7b94a53

9 files changed

Lines changed: 419 additions & 13 deletions

File tree

conda/environments/all_cuda-122.yaml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,8 @@ dependencies:
2828
- ninja>=1.11.1.1
2929
- notebook>=7
3030
- numpy
31+
- onnx>=1.10
32+
- onnxmltools>=1.10
3133
- openblas
3234
- pydata-sphinx-theme>=0.16
3335
- pytest>=7,<8

dependencies.yaml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -176,3 +176,5 @@ dependencies:
176176
- pytest>=7,<8
177177
- seaborn>=0.13
178178
- xgboost>=2.0
179+
- onnx>=1.10
180+
- onnxmltools>=1.10

legateboost/models/base_model.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -126,3 +126,13 @@ def __mul__(self, scalar: Any) -> "BaseModel":
126126

127127
def __hash__(self) -> int:
128128
return hash(str(self))
129+
130+
def to_onnx(self) -> Any:
131+
"""Convert the model to an ONNX model.
132+
133+
Returns
134+
-------
135+
Any
136+
The ONNX model.
137+
"""
138+
raise NotImplementedError

legateboost/models/krr.py

Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -242,3 +242,116 @@ def __mul__(self, scalar: Any) -> "KRR":
242242
new = copy.deepcopy(self)
243243
self.betas_ *= scalar
244244
return new
245+
246+
def to_onnx(self) -> Any:
247+
from onnx import numpy_helper
248+
from onnx.checker import check_model
249+
from onnx.helper import (
250+
make_graph,
251+
make_model,
252+
make_node,
253+
make_tensor_value_info,
254+
np_dtype_to_tensor_dtype,
255+
)
256+
257+
assert self.X_train.dtype == self.betas_.dtype
258+
259+
def make_constant_node(value: cn.array, name: str) -> Any:
260+
return make_node(
261+
"Constant",
262+
inputs=[],
263+
value=numpy_helper.from_array(value, name=name),
264+
outputs=[name],
265+
)
266+
267+
nodes = []
268+
269+
# model constants
270+
betas = numpy_helper.from_array(self.betas_.__array__(), name="betas")
271+
X_train = numpy_helper.from_array(self.X_train.__array__(), name="X_train")
272+
273+
# pred inputs
274+
X = make_tensor_value_info(
275+
"X",
276+
np_dtype_to_tensor_dtype(self.betas_.dtype),
277+
[None, self.X_train.shape[1]],
278+
)
279+
pred = make_tensor_value_info(
280+
"pred",
281+
np_dtype_to_tensor_dtype(self.betas_.dtype),
282+
[None, self.betas_.shape[1]],
283+
)
284+
285+
# exanded l2 distance
286+
# distance = np.sum(X**2, axis=1)[:, np.newaxis] - 2 * np.dot(X, self.X_train.T)
287+
# + np.sum(self.X_train**2, axis=1)
288+
make_tensor_value_info(
289+
"XX", np_dtype_to_tensor_dtype(self.betas_.dtype), [None]
290+
)
291+
make_tensor_value_info(
292+
"YY",
293+
np_dtype_to_tensor_dtype(self.betas_.dtype),
294+
[self.X_train.shape[0], 1],
295+
)
296+
make_tensor_value_info(
297+
"XY_reshaped",
298+
np_dtype_to_tensor_dtype(self.betas_.dtype),
299+
[1, self.X_train.shape[0]],
300+
)
301+
make_tensor_value_info(
302+
"XY",
303+
np_dtype_to_tensor_dtype(self.betas_.dtype),
304+
[None, self.X_train.shape[0]],
305+
)
306+
nodes.append(make_constant_node(np.array([1]), "axis1"))
307+
nodes.append(make_node("ReduceSumSquare", ["X", "axis1"], ["XX"]))
308+
nodes.append(make_node("Gemm", ["X", "X_train"], ["XY"], alpha=-2.0, transB=1))
309+
nodes.append(make_node("ReduceSumSquare", ["X_train", "axis1"], ["YY"]))
310+
nodes.append(make_constant_node(np.array([1, -1]), "reshape"))
311+
nodes.append(make_node("Reshape", ["YY", "reshape"], ["YY_reshaped"]))
312+
nodes.append(make_node("Add", ["XX", "XY"], ["add0"]))
313+
make_tensor_value_info(
314+
"l2",
315+
np_dtype_to_tensor_dtype(self.betas_.dtype),
316+
[None, self.X_train.shape[0]],
317+
)
318+
nodes.append(make_node("Add", ["YY_reshaped", "add0"], ["l2"]))
319+
nodes.append(make_constant_node(np.array([0.0], self.betas_.dtype), "zero"))
320+
make_tensor_value_info(
321+
"l2_clipped",
322+
np_dtype_to_tensor_dtype(self.betas_.dtype),
323+
[None, self.X_train.shape[0]],
324+
)
325+
nodes.append(make_node("Max", ["l2", "zero"], ["l2_clipped"]))
326+
327+
# RBF kernel
328+
# K = np.exp(-distance / (2 * self.sigma**2))
329+
make_tensor_value_info(
330+
"rbf0",
331+
np_dtype_to_tensor_dtype(self.betas_.dtype),
332+
[None, self.X_train.shape[0]],
333+
)
334+
if self.sigma is None:
335+
raise ValueError("sigma is None. Has fit been called?")
336+
nodes.append(
337+
make_constant_node(
338+
np.array([-2.0 * self.sigma**2], self.betas_.dtype), "denominator"
339+
)
340+
)
341+
nodes.append(make_node("Div", ["l2_clipped", "denominator"], ["rbf0"]))
342+
make_tensor_value_info(
343+
"K",
344+
np_dtype_to_tensor_dtype(self.betas_.dtype),
345+
[None, self.X_train.shape[0]],
346+
)
347+
nodes.append(make_node("Exp", ["rbf0"], ["K"]))
348+
349+
# prediction
350+
# pred = np.dot(K, self.betas_)
351+
nodes.append(make_node("MatMul", ["K", "betas"], ["pred"]))
352+
graph = make_graph(
353+
nodes, "legateboost.model.KRR", [X], [pred], [betas, X_train]
354+
)
355+
onnx_model = make_model(graph)
356+
check_model(onnx_model)
357+
return onnx_model

legateboost/models/linear.py

Lines changed: 38 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,7 @@ def __init__(
5858
self.l2_regularization = alpha
5959

6060
def _fit_solve(self, X: cn.ndarray, g: cn.ndarray, h: cn.ndarray) -> None:
61-
self.betas_ = cn.zeros((X.shape[1] + 1, g.shape[1]))
61+
self.betas_ = cn.zeros((X.shape[1] + 1, g.shape[1]), dtype=X.dtype)
6262
num_outputs = g.shape[1]
6363
for k in range(num_outputs):
6464
W = cn.sqrt(h[:, k])
@@ -135,12 +135,13 @@ def batch_predict(models: Sequence[BaseModel], X: cn.ndarray) -> cn.ndarray:
135135
# summing together the coeffiecients of each model then predicting
136136
# saves a lot of work
137137
betas = cn.sum([model.betas_ for model in models], axis=0)
138-
return betas[0] + X.dot(betas[1:].astype(X.dtype))
138+
betas = betas.astype(X.dtype)
139+
return betas[0] + X.dot(betas[1:])
139140

140141
def __str__(self) -> str:
141142
return (
142143
"Bias: "
143-
+ str(self.betas_[1])
144+
+ str(self.betas_[0])
144145
+ "\nCoefficients: "
145146
+ str(self.betas_[1:])
146147
+ "\n"
@@ -150,3 +151,37 @@ def __mul__(self, scalar: Any) -> "Linear":
150151
new = copy.deepcopy(self)
151152
new.betas_ *= scalar
152153
return new
154+
155+
def to_onnx(self) -> Any:
156+
from onnx import numpy_helper
157+
from onnx.checker import check_model
158+
from onnx.helper import (
159+
make_graph,
160+
make_model,
161+
make_node,
162+
make_tensor_value_info,
163+
np_dtype_to_tensor_dtype,
164+
)
165+
166+
# model constants
167+
betas = numpy_helper.from_array(self.betas_[1:].__array__(), name="betas")
168+
intercept = numpy_helper.from_array(
169+
self.betas_[0].__array__(), name="intercept"
170+
)
171+
172+
# pred inputs
173+
X = make_tensor_value_info(
174+
"X", np_dtype_to_tensor_dtype(self.betas_.dtype), [None, None]
175+
)
176+
pred = make_tensor_value_info(
177+
"pred", np_dtype_to_tensor_dtype(self.betas_.dtype), [None]
178+
)
179+
180+
node1 = make_node("MatMul", ["X", "betas"], ["XBeta"])
181+
node2 = make_node("Add", ["XBeta", "intercept"], ["pred"])
182+
graph = make_graph(
183+
[node1, node2], "legateboost.model.Linear", [X], [pred], [betas, intercept]
184+
)
185+
onnx_model = make_model(graph)
186+
check_model(onnx_model)
187+
return onnx_model

legateboost/models/nn.py

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -181,3 +181,87 @@ def __mul__(self, scalar: Any) -> "NN":
181181
new.coefficients_[-1] *= scalar
182182
new.biases_[-1] *= scalar
183183
return new
184+
185+
def to_onnx(self) -> Any:
186+
from onnx import numpy_helper
187+
from onnx.checker import check_model
188+
from onnx.helper import (
189+
make_graph,
190+
make_model,
191+
make_node,
192+
make_tensor_value_info,
193+
np_dtype_to_tensor_dtype,
194+
)
195+
196+
# model constants
197+
biases = [
198+
numpy_helper.from_array(b[0].__array__(), name=f"bias{i}")
199+
for i, b in enumerate(self.biases_)
200+
]
201+
coefficients = [
202+
numpy_helper.from_array(c.__array__(), name=f"coefficients{i}")
203+
for i, c in enumerate(self.coefficients_)
204+
]
205+
206+
# pred inputs
207+
X = make_tensor_value_info(
208+
"X",
209+
np_dtype_to_tensor_dtype(self.coefficients_[0].dtype),
210+
[None, self.coefficients_[0].shape[0]],
211+
)
212+
213+
nodes = []
214+
215+
make_tensor_value_info(
216+
"activations0",
217+
np_dtype_to_tensor_dtype(self.coefficients_[0].dtype),
218+
[None, None],
219+
)
220+
nodes.append(make_node("MatMul", ["X", "coefficients0"], ["activations0"]))
221+
activations_with_bias = make_tensor_value_info(
222+
"activations0withbias",
223+
np_dtype_to_tensor_dtype(self.coefficients_[0].dtype),
224+
[None, None],
225+
)
226+
nodes.append(
227+
make_node("Add", ["activations0", "bias0"], ["activations0withbias"])
228+
)
229+
230+
for i in range(1, len(coefficients)):
231+
make_tensor_value_info(
232+
f"tanh{i}",
233+
np_dtype_to_tensor_dtype(self.coefficients_[0].dtype),
234+
[None, None],
235+
)
236+
nodes.append(make_node("Tanh", [f"activations{i-1}withbias"], [f"tanh{i}"]))
237+
make_tensor_value_info(
238+
f"activations{i}",
239+
np_dtype_to_tensor_dtype(self.coefficients_[0].dtype),
240+
[None, None],
241+
)
242+
nodes.append(
243+
make_node(
244+
"MatMul", [f"tanh{i}", f"coefficients{i}"], [f"activations{i}"]
245+
)
246+
)
247+
activations_with_bias = make_tensor_value_info(
248+
f"activations{i}withbias",
249+
np_dtype_to_tensor_dtype(self.coefficients_[0].dtype),
250+
[None, None],
251+
)
252+
nodes.append(
253+
make_node(
254+
"Add", [f"activations{i}", f"bias{i}"], [f"activations{i}withbias"]
255+
)
256+
)
257+
258+
graph = make_graph(
259+
nodes,
260+
"legateboost.model.NN",
261+
[X],
262+
[activations_with_bias],
263+
biases + coefficients,
264+
)
265+
onnx_model = make_model(graph)
266+
check_model(onnx_model)
267+
return onnx_model

0 commit comments

Comments
 (0)