Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ jobs:

- name: Install brew formulae (ubuntu)
if: startsWith(matrix.os, 'ubuntu')
run: /home/linuxbrew/.linuxbrew/bin/brew install mlpack openblas
run: /home/linuxbrew/.linuxbrew/bin/brew install libsvm mlpack openblas

- name: Check that workflows are up to date
run: sbt githubWorkflowCheck
Expand Down
4 changes: 2 additions & 2 deletions build.sbt
Original file line number Diff line number Diff line change
Expand Up @@ -67,9 +67,9 @@ lazy val runtime = project
.settings(
name := "vilcacora-runtime",
libraryDependencies ++= Seq(
"org.typelevel" %%% "cats-effect-kernel" % CatsEffectVersion,
"org.typelevel" %%% "cats-effect" % CatsEffectVersion,
"org.typelevel" %%% "cats-core" % CatsVersion,
"org.scalameta" %%% "munit" % MunitVersion % Test,
),
nativeBrewFormulas ++= Set("openblas", "mlpack"),
nativeBrewFormulas ++= Set("openblas", "mlpack", "libsvm"),
)
68 changes: 63 additions & 5 deletions onnx/src/main/scala/vilcacora/onnx/Translator.scala
Original file line number Diff line number Diff line change
Expand Up @@ -93,11 +93,69 @@ object Translator {
// Create allocations for constant tensors, which include initial data.
val initializerAllocations: Either[String, List[Allocation]] =
graph.initializer.toList.traverse(createAllocationFromInitializer)
// This new section manually creates allocations for intermediate tensors
// that are not explicitly declared in the ONNX graph's value_info.

val manuallyCreatedAllocs = for {
valAllocs <- valueAllocations
initAllocs <- initializerAllocations
// Create a temporary map of all known allocations so far for lookups.
existingAllocs = (valAllocs ++ initAllocs).map(a => a.name -> a).toMap

// Iterate through all nodes to find any that need special handling.
newAllocs <- graph.node.toList.flatTraverse { node =>
node.opType match {
case "SVMClassifier" =>
for {
_ <- checkArity(node, 1, 2) // Ensure SVMClassifier has 2 outputs
scoresOutputName = node.output(1)
Comment on lines +108 to +111

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Some of the repeated logic is unfortunate here. Not for this PR, but in a follow-up PR I wonder if we can combine both these phases in single-pass. So, the translator would generate both IR operations and manual allocations simultaneously as it traverses the ONNX operations.

// Check if an allocation for the scores tensor already exists.
allocations <-
if (existingAllocs.contains(scoresOutputName)) {
// If it exists, we don't need to do anything.
Right(List.empty[Allocation])
} else {
// If it doesn't exist, create it manually.
for {
// Get the input tensor's allocation to infer the batch size.
inputAlloc <- existingAllocs
.get(node.input.head)
.toRight(
s"SVM input '${node.input.head}' not found in allocations.",
)
batchSize <- inputAlloc.shape.headOption.toRight(
s"Input '${inputAlloc.name}' for SVM has no dimensions.",
)

// Get the number of classes from the node's attributes.
attributes = new OnnxAttributeHelper(node)
classLabels <- attributes.getInts("classlabels_ints")
numClasses = classLabels.size

// The ONNX spec defines the scores output as a float tensor.
// We default to Float32. Shape is [batch_size, num_classes].
scoresAlloc = Allocation(
name = scoresOutputName,
dataType = DataType.Float32,
shape = List(batchSize.toInt, numClasses),
initialData = None,
)
} yield List(scoresAlloc)
}
} yield allocations

case _ =>
// For all other operators, we assume their outputs are properly declared.
Right(List.empty[Allocation])
}
}
} yield newAllocs

for {
valAllocs <- valueAllocations
initAllocs <- initializerAllocations
} yield (valAllocs ++ initAllocs).map(a => a.name -> a).toMap
manualAllocs <- manuallyCreatedAllocs
} yield (valAllocs ++ initAllocs ++ manualAllocs).map(a => a.name -> a).toMap
}

/** Translates a single ONNX `NodeProto` into its corresponding IR `Operation`.
Expand Down Expand Up @@ -223,8 +281,8 @@ object Translator {
case 11 => Right(DataType.Float64)
case 10 => Right(DataType.Float16)
case 16 => Right(DataType.BFloat16)
case 7 => Right(DataType.Int32)
case 6 => Right(DataType.Int64)
case 6 => Right(DataType.Int32)
case 7 => Right(DataType.Int64)
case 5 => Right(DataType.Int16)
case 3 => Right(DataType.Int8)
case 12 => Right(DataType.UInt32)
Expand Down Expand Up @@ -256,8 +314,8 @@ object Translator {
tensor.dataType match {
case 1 => tensor.floatData.foreach(buffer.putFloat)
case 11 => tensor.doubleData.foreach(buffer.putDouble)
case 7 => tensor.int32Data.foreach(buffer.putInt)
case 6 => tensor.int64Data.foreach(buffer.putLong)
case 6 => tensor.int32Data.foreach(buffer.putInt)
case 7 => tensor.int64Data.foreach(buffer.putLong)
// Note: Other types like int16 are typically stored in `rawData` or `int32Data`.
case unsupportedType =>
return Left(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,9 +28,10 @@ class TranslatorInternalsSuite extends FunSuite {

test("fromOnnxDataType should map known type codes") {
assertEquals(Translator.fromOnnxDataType(1), Right(DataType.Float32))
assertEquals(Translator.fromOnnxDataType(7), Right(DataType.Int32))
// ONNX code 6 is INT32, 7 is INT64
assertEquals(Translator.fromOnnxDataType(6), Right(DataType.Int32))
assertEquals(Translator.fromOnnxDataType(7), Right(DataType.Int64))
}

test("fromOnnxDataType should fail on unknown type codes") {
assert(Translator.fromOnnxDataType(999).isLeft)
}
Expand Down
4 changes: 2 additions & 2 deletions onnx/src/test/scala/vilcacora/onnx/TranslatorSuite.scala
Original file line number Diff line number Diff line change
Expand Up @@ -42,8 +42,8 @@ class TranslatorSuite extends FunSuite {
input = Seq("in"),
output = Seq("out"),
attribute = Seq(
AttributeProto(name = "to", i = 6, `type` = AttributeProto.AttributeType.INT),
), // 6 = INT64
AttributeProto(name = "to", i = 7, `type` = AttributeProto.AttributeType.INT),
), // 7 = INT64
)
assertEquals(Translator.translateNode(node), Right(Operation.Cast("in", "out", DataType.Int64)))
}
Expand Down
202 changes: 202 additions & 0 deletions runtime/src/main/resources/scala-native/svm_wrapper.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,202 @@
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <math.h>
#include "svm.h"

// Wrapper functions for LibSVM struct creation and management
extern "C" {

// Create and initialize svm_parameter
struct svm_parameter* create_svm_param(
int svm_type,
int kernel_type,
int degree,
double gamma,
double coef0
) {
struct svm_parameter *param = (struct svm_parameter *)malloc(sizeof(struct svm_parameter));
if (!param) return NULL;

// Initialize all fields to safe defaults
memset(param, 0, sizeof(struct svm_parameter));

// Set provided values
param->svm_type = svm_type;
param->kernel_type = kernel_type;
param->degree = degree;
param->gamma = gamma;
param->coef0 = coef0;



return param;
}

// Create svm_model with proper initialization
struct svm_model* create_svm_model(
struct svm_parameter *param,
int nr_class,
int l,
double *support_vectors, // flattened array [l * num_features]
int num_features,
double *coefficients, // flattened array [(nr_class-1) * l]
double *rho, // array [nr_class*(nr_class-1)/2]
int *class_labels, // array [nr_class]
int *n_sv_per_class // array [nr_class]
) {
struct svm_model *model = (struct svm_model *)malloc(sizeof(struct svm_model));
if (!model) return NULL;

// Initialize all fields
memset(model, 0, sizeof(struct svm_model));

// Basic model properties
model->param = *param; // Copy parameter struct
model->nr_class = nr_class;
model->l = l;

// Allocate and populate support vectors
model->SV = (struct svm_node **)malloc(sizeof(struct svm_node*) * l);
for (int i = 0; i < l; i++) {
model->SV[i] = (struct svm_node *)malloc(sizeof(struct svm_node) * (num_features + 1));

// Copy feature values
for (int j = 0; j < num_features; j++) {
model->SV[i][j].index = j + 1; // 1-indexed
model->SV[i][j].value = support_vectors[i * num_features + j];
}
// Terminator node
model->SV[i][num_features].index = -1;
model->SV[i][num_features].value = 0.0;
}

// Allocate and populate coefficients
model->sv_coef = (double **)malloc(sizeof(double*) * (nr_class - 1));
for (int i = 0; i < nr_class - 1; i++) {
model->sv_coef[i] = (double *)malloc(sizeof(double) * l);
for (int j = 0; j < l; j++) {
model->sv_coef[i][j] = coefficients[i * l + j];
}
}

// Copy rho (bias terms)
int rho_size = nr_class * (nr_class - 1) / 2;
model->rho = (double *)malloc(sizeof(double) * rho_size);
memcpy(model->rho, rho, sizeof(double) * rho_size);

// Copy class labels
model->label = (int *)malloc(sizeof(int) * nr_class);
memcpy(model->label, class_labels, sizeof(int) * nr_class);

// Copy number of SVs per class
model->nSV = (int *)malloc(sizeof(int) * nr_class);
memcpy(model->nSV, n_sv_per_class, sizeof(int) * nr_class);

// Initialize other fields
model->probA = NULL;
model->probB = NULL;
model->sv_indices = NULL;
model->free_sv = 1;

return model;
}

// Prediction with per-class scores
int svm_predict_with_scores(
struct svm_model *model,
double *features, // input features [num_features]
int num_features,
double *class_scores // output scores [nr_class]
) {
// Create input svm_node array
struct svm_node *x = (struct svm_node *)malloc(sizeof(struct svm_node) * (num_features + 1));

for (int i = 0; i < num_features; i++) {
x[i].index = i + 1;
x[i].value = features[i];
}
x[num_features].index = -1; // terminator

// Get decision values from LibSVM
int nr_class = model->nr_class;
int dec_values_count = (nr_class * (nr_class - 1)) / 2;
double *dec_values = (double *)malloc(sizeof(double) * dec_values_count);

double predicted_label = svm_predict_values(model, x, dec_values);

// Convert to per-class scores
if (nr_class == 2) {
// Binary case
class_scores[0] = -dec_values[0];
class_scores[1] = dec_values[0];
} else {
// Multiclass: OvO to OvR conversion
int *votes = (int *)calloc(nr_class, sizeof(int));
double *conf = (double *)calloc(nr_class, sizeof(double));

int k = 0;
for (int i = 0; i < nr_class; i++) {
for (int j = i + 1; j < nr_class; j++) {
double margin = dec_values[k];

if (margin > 0) {
votes[i] += 1;
} else {
votes[j] += 1;
}

conf[i] -= margin;
conf[j] += margin;
k++;
}
}

// Apply tie-breaking and final scores
for (int c = 0; c < nr_class; c++) {
double tconf = conf[c] / (3.0 * (fabs(conf[c]) + 1.0));
class_scores[c] = (double)votes[c] + tconf;
}

free(votes);
free(conf);
}

free(x);
free(dec_values);

return (int)predicted_label;
}

// Debug function to print model details
void debug_model_info(struct svm_model *model) {
printf("=== SVM Model Debug Info ===\n");
printf("nr_class: %d\n", model->nr_class);
printf("l (num support vectors): %d\n", model->l);
printf("kernel_type: %d\n", model->param.kernel_type);
printf("gamma: %f\n", model->param.gamma);
printf("coef0: %f\n", model->param.coef0);
printf("degree: %d\n", model->param.degree);

printf("Class labels: ");
for (int i = 0; i < model->nr_class; i++) {
printf("%d ", model->label[i]);
}
printf("\n");

printf("Number of SVs per class: ");
for (int i = 0; i < model->nr_class; i++) {
printf("%d ", model->nSV[i]);
}
printf("\n");

printf("Rho values: ");
int rho_size = model->nr_class * (model->nr_class - 1) / 2;
for (int i = 0; i < rho_size; i++) {
printf("%f ", model->rho[i]);
}
printf("\n");
printf("===========================\n");
}

} // extern "C"
Loading