Skip to content
Draft
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
26 changes: 24 additions & 2 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -341,26 +341,48 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
- uses: dtolnay/rust-toolchain@stable
- uses: r-lib/actions/setup-r@v2
- name: Install R dependencies
run: Rscript -e 'install.packages("jsonlite", repos = "https://cloud.r-project.org")'
- name: Build DAG-ML C ABI
run: cargo build -p dag-ml-capi --release
- name: Build R source package
run: |
mkdir -p target/r
cd target/r
R CMD build ../../bindings/r
- name: Check R source package
run: R CMD check --no-manual target/r/dagml_*.tar.gz
run: |
if ! R CMD check --no-manual target/r/dagml_*.tar.gz; then
for log in dagml.Rcheck/00install.out dagml.Rcheck/00check.log; do
if [ -f "$log" ]; then
printf '\n===== %s =====\n' "$log"
sed -n '1,240p' "$log"
fi
done
exit 1
fi
env:
DAGML_NATIVE_LIBRARY: ${{ github.workspace }}/target/release/libdag_ml_capi.so

matlab-octave-bindings:
name: MATLAB/Octave bindings
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
- uses: dtolnay/rust-toolchain@stable
- name: Install GNU Octave
run: sudo apt-get update && sudo apt-get install --yes octave
run: sudo apt-get update && sudo apt-get install --yes octave octave-dev
- name: Build native task-binding bridges
run: |
cargo build -p dag-ml-capi --release
mkoctfile --mex bindings/matlab/native/task_training_loss_binding.c \
-o bindings/matlab/+dagml/taskTrainingLossBindingNative.mex
- name: Test local implementation registry
run: octave --no-gui --quiet --eval "addpath('bindings/matlab/tests'); local_implementation_registry"
env:
DAGML_NATIVE_LIBRARY: ${{ github.workspace }}/target/release/libdag_ml_capi.so

wasm-bindings:
name: WASM bindings Node ${{ matrix.node-version }}
Expand Down
134 changes: 35 additions & 99 deletions bindings/matlab/+dagml/LocalImplementationRegistry.m
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,17 @@
properties (Access = private)
BindingId = 'binding:matlab'
Entries
NativeLibrary
end

methods
function self = LocalImplementationRegistry()
function self = LocalImplementationRegistry(nativeLibrary)
if nargin < 1
nativeLibrary = getenv('DAGML_NATIVE_LIBRARY');
end
self.NativeLibrary = ...
dagml.LocalImplementationRegistry.optionalScalarText( ...
nativeLibrary, 'native library');
self.Entries = containers.Map('KeyType', 'char', 'ValueType', 'any');
end

Expand Down Expand Up @@ -54,45 +61,27 @@ function registerMetric(self, metricReference, implementation)
if nargin < 3 || isempty(roleIndex)
roleIndex = 1;
end
if ~isstruct(task) || ~isscalar(task) || ~isfield(task, 'node_plan') || ...
~isstruct(task.node_plan) || ~isscalar(task.node_plan)
error('dagml:LocalImplementationRegistry:InvalidTask', ...
'Training loss invocation requires a DAG-ML NodeTask.');
end

phase = dagml.LocalImplementationRegistry.scalarText(task.phase, 'task phase');
if ~any(strcmp(phase, {'FIT_CV', 'REFIT'}))
error('dagml:LocalImplementationRegistry:InvalidPhase', ...
'Training loss phase must be FIT_CV or REFIT.');
end

roles = dagml.LocalImplementationRegistry.collection(task.node_plan, 'training_losses');
activeRoles = {};
for index = 1:numel(roles)
if dagml.LocalImplementationRegistry.roleApplies(roles{index}, phase)
activeRoles{end + 1} = roles{index}; %#ok<AGROW>
end
end
requirements = dagml.LocalImplementationRegistry.collection( ...
task, 'required_loss_attestations');
if numel(activeRoles) ~= numel(requirements)
error('dagml:LocalImplementationRegistry:RequirementCount', ...
'Task loss execution requirement count does not match active roles.');
end
if ~isnumeric(roleIndex) || ~isscalar(roleIndex) || ...
~isfinite(roleIndex) || fix(roleIndex) ~= roleIndex || ...
roleIndex < 1 || roleIndex > numel(activeRoles)
roleIndex < 1 || roleIndex > flintmax
error('dagml:LocalImplementationRegistry:RoleIndex', ...
'roleIndex is outside the active training loss range.');
'roleIndex must be a positive safe integer.');
end

role = activeRoles{roleIndex};
requirement = requirements{roleIndex};
dagml.LocalImplementationRegistry.validateRequirement(task, role, requirement);
implementation = self.resolveTrainingLoss(role, phase);
if isempty(self.NativeLibrary)
error('dagml:LocalImplementationRegistry:NativeLibrary', ...
['Native DAG-ML library path is required; pass it to the ' ...
'constructor or set DAGML_NATIVE_LIBRARY.']);
end
taskJSON = dagml.LocalImplementationRegistry.nodeTaskJSON(task);
[roleJSON, attestationJSON] = ...
dagml.taskTrainingLossBindingNative( ...
taskJSON, roleIndex - 1, self.NativeLibrary);
role = jsondecode(roleJSON);
requiredAttestation = jsondecode(attestationJSON);
implementation = self.resolveImplementation(role.loss, 'loss');

value = implementation(varargin{:});
attestation = requirement;
attestation = requiredAttestation;
end

function implementation = unregisterLoss(self, lossReference)
Expand Down Expand Up @@ -237,24 +226,22 @@ function registerImplementation(self, reference, implementation, semanticKind)
end
end

function result = collection(value, field)
if ~isstruct(value) || ~isscalar(value) || ~isfield(value, field) || ...
isempty(value.(field))
result = {};
return;
end
raw = value.(field);
if iscell(raw)
result = reshape(raw, 1, []);
elseif isstruct(raw)
result = arrayfun(@(item) item, raw, 'UniformOutput', false);
result = reshape(result, 1, []);
function result = optionalScalarText(value, label)
if ischar(value) && (isrow(value) || isempty(value))
result = value;
elseif isstring(value) && isscalar(value)
result = char(value);
else
error('dagml:LocalImplementationRegistry:InvalidCollection', ...
'%s must be an array of objects.', field);
error('dagml:LocalImplementationRegistry:InvalidText', ...
'%s must be scalar text.', label);
end
end

function result = nodeTaskJSON(task)
result = dagml.LocalImplementationRegistry.scalarText( ...
task, 'NodeTask JSON');
end

function result = roleApplies(role, phase)
if ~isstruct(role) || ~isscalar(role) || ~isfield(role, 'phases')
error('dagml:LocalImplementationRegistry:InvalidRole', ...
Expand All @@ -278,57 +265,6 @@ function registerImplementation(self, reference, implementation, semanticKind)
end
end

function validateRequirement(task, role, requirement)
if ~isstruct(requirement) || ~isscalar(requirement)
error('dagml:LocalImplementationRegistry:InvalidRequirement', ...
'Loss execution requirement must be an object.');
end
if ~isfield(role, 'loss') || ~isstruct(role.loss) || ...
~isfield(role.loss, 'spec') || ~isfield(role.loss, 'implementation')
error('dagml:LocalImplementationRegistry:InvalidRole', ...
'Training loss role contains an invalid loss reference.');
end
if ~isfield(role, 'node_id') || ...
~dagml.LocalImplementationRegistry.sameJSONValue( ...
role.node_id, task.node_plan.node_id)
error('dagml:LocalImplementationRegistry:NodeMismatch', ...
'Training loss role node_id does not match the task node.');
end
if ~isfield(requirement, 'schema_version') || ...
~dagml.LocalImplementationRegistry.sameJSONValue( ...
requirement.schema_version, 1)
error('dagml:LocalImplementationRegistry:SchemaVersion', ...
'Loss execution requirement schema_version must be 1.');
end

spec = role.loss.spec;
implementation = role.loss.implementation;
expectedFields = { ...
'node_id', task.node_plan.node_id; ...
'output_id', role.output_id; ...
'phase', task.phase; ...
'loss_id', spec.loss_id; ...
'semantic_fingerprint', spec.spec_fingerprint; ...
'implementation_fingerprint', implementation.implementation_fingerprint; ...
'descriptor_fingerprint', implementation.descriptor_fingerprint; ...
'effective_parameters', spec.parameters; ...
'reduction', spec.reduction ...
};
for index = 1:size(expectedFields, 1)
field = expectedFields{index, 1};
if ~isfield(requirement, field) || ...
~dagml.LocalImplementationRegistry.sameJSONValue( ...
requirement.(field), expectedFields{index, 2})
error('dagml:LocalImplementationRegistry:RequirementMismatch', ...
['Loss execution requirement field ''%s'' does not match ' ...
'the training role.'], field);
end
end
dagml.LocalImplementationRegistry.requiredText( ...
requirement, 'attestation_fingerprint', ...
'loss execution requirement attestation_fingerprint');
end

function result = sameJSONValue(left, right)
if isstruct(left) || isstruct(right)
if ~isstruct(left) || ~isstruct(right) || ...
Expand Down
37 changes: 30 additions & 7 deletions bindings/matlab/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,19 +6,42 @@ under exact DAG-ML descriptors; executable objects are never written into DAG
JSON contracts.

```matlab
implementations = dagml.LocalImplementationRegistry();
implementations = dagml.LocalImplementationRegistry( ...
getenv('DAGML_NATIVE_LIBRARY'));
implementations.registerLoss(lossReference, @asymmetricLoss);

[value, attestation] = implementations.invokeTrainingLoss( ...
nodeTask, 1, yTrue, yPred);
nodeTaskJSON, 1, yTrue, yPred);
result.lineage.loss_attestations = {attestation};
```

`invokeTrainingLoss` accepts only `FIT_CV` and `REFIT`. It validates the active
role against `NodeTask.required_loss_attestations`, executes the local function,
and returns the native-produced attestation only after successful execution.
Each MATLAB process, parallel worker, Octave process, or replay process must
register its own local functions.
`invokeTrainingLoss` accepts the exact `NodeTask` JSON emitted by DAG-ML. This
avoids ambiguous host round-trips for single-element JSON arrays. The MEX bridge
asks the DAG-ML C ABI to select the phase-filtered role and task-owned
attestation, then executes the function handle on MATLAB-owned values.
`PREDICT`, stale attestations, and invalid role indexes fail in the native core
before the function handle runs. Each MATLAB process, parallel worker, Octave
process, or replay process must load the DAG-ML native library and register its
own local functions.

Build the native library and MATLAB MEX bridge with:

```bash
cargo build -p dag-ml-capi --release
export DAGML_NATIVE_LIBRARY="$PWD/target/release/libdag_ml_capi.so"
```

```matlab
addpath('bindings/matlab');
buildNativeBinding
```

For GNU Octave on Linux:

```bash
mkoctfile --mex bindings/matlab/native/task_training_loss_binding.c \
-o bindings/matlab/+dagml/taskTrainingLossBindingNative.mex
```

Run the binding test with GNU Octave:

Expand Down
11 changes: 11 additions & 0 deletions bindings/matlab/buildNativeBinding.m
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
function outputPath = buildNativeBinding()
% Build the DAG-ML task-binding MEX bridge for MATLAB.

bindingRoot = fileparts(mfilename('fullpath'));
sourcePath = fullfile(bindingRoot, 'native', 'task_training_loss_binding.c');
outputDirectory = fullfile(bindingRoot, '+dagml');
mex('-outdir', outputDirectory, '-output', ...
'taskTrainingLossBindingNative', sourcePath);
outputPath = fullfile(outputDirectory, ...
['taskTrainingLossBindingNative.' mexext]);
end
Loading