Skip to content

Run a learned MLP policy forward without copying its parameters - #340

Open
Thiago316316 wants to merge 5 commits into
kmolan:mainfrom
Thiago316316:fix/83-mlp-inference-core
Open

Run a learned MLP policy forward without copying its parameters#340
Thiago316316 wants to merge 5 commits into
kmolan:mainfrom
Thiago316316:fix/83-mlp-inference-core

Conversation

@Thiago316316

Copy link
Copy Markdown
Contributor

What & why

A trained policy reaches a robot as one flat block of numbers. Two hidden layers 64 units wide over a 22-component observation come to about 5,900 of them, some 23 KB as f32, against the 64 KB of RAM a small Cortex-M has in total.

Layer holds a MatrixView of its weights and a VectorView of its biases instead, so the coefficients are read where they were stored. forward writes only the activations, OUTPUT of them rather than OUTPUTxINPUT. Activation carries the scalar nonlinearity as an enum rather than a fn pointer, so the choice is inlinable and a layer stays inspectable; it is #[non_exhaustive], leaving room for more.

Widths are const parameters, so chaining a layer that produces three values into one that expects four fails to build rather than at runtime. Nothing allocates and nothing panics, so it runs under no_std.

issue #83

Checklist

  • cargo test + cargo clippy --all-targets clean locally
  • New public APIs have a doc example
  • No unwrap/expect/panic on library paths (typed errors instead)

A trained policy reaches a robot as one flat block of numbers. Two
hidden layers 64 units wide over a 22-component observation come to
about 5,900 of them, some 23 KB as `f32`, against the 64 KB of RAM a
small Cortex-M has in total. Owning those weights would copy the block
onto the stack every control cycle.

`Layer` holds a `MatrixView` of its weights and a `VectorView` of its
biases instead, so the coefficients are read where they were stored.
`forward` writes only the activations, `OUTPUT` of them rather than
`OUTPUT`x`INPUT`. `Activation` carries the scalar nonlinearity as an
enum rather than a `fn` pointer, so the choice is inlinable and a layer
stays inspectable; it is `#[non_exhaustive]`, leaving room for more.

Widths are const parameters, so chaining a layer that produces three
values into one that expects four fails to build rather than at runtime.
Nothing allocates and nothing panics, so it runs under `no_std`.

Inference only. Training belongs on a machine with room for it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@Thiago316316
Thiago316316 requested a review from kmolan as a code owner August 28, 2026 15:50
The overview ran three long paragraphs before the example, restating in
prose what the example shows. Cut to what a reader cannot get from the
code: what the layers do, why the parameters are borrowed, and what the
const widths buy.

The links go with it. `cargo doc` denies warnings in CI, and rustdoc
counts a link whose label already names a path in scope as a redundant
explicit target. Both view types are imported here, so the bare labels
resolve to the same pages the spelled-out paths did.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@Thiago316316
Thiago316316 force-pushed the fix/83-mlp-inference-core branch from a38ddda to 6a32e97 Compare August 28, 2026 23:20
@kmolan

kmolan commented Sep 3, 2026

Copy link
Copy Markdown
Owner

@Thiago316316 please resolve conflicts and update to main

@Thiago316316

Copy link
Copy Markdown
Contributor Author

hi @kmolan one of the CI checks got stuck, please can you help me?

Comment thread README.md Outdated
- [Kinematics](https://github.com/kmolan/multicalc-rust/blob/main/crates/multicalc/tutorials/kinematics.md): `KinematicTree` for revolute/prismatic/continuous/fixed/floating chains and forward and inverse kinematics, generic over the scalar with autodiff, built by hand or read from an MJCF or URDF file; a damped-least-squares SE(3) pose solver with joint limits and null-space redundancy resolution.
- [Collision checking](https://github.com/kmolan/multicalc-rust/blob/main/crates/multicalc/tutorials/kinematics.md#collision-checking): `CollisionQuery` for sphere/capsule proximity — primitives on tree frames against each other and against world-fixed obstacles, with pair exclusions and fixed capacities.
- [Motion](https://github.com/kmolan/multicalc-rust/blob/main/crates/multicalc/tutorials/motion.md): `PolylinePath` for waypoint paths with arc-length, closest-point, and lookahead queries, `MinimumSnapPlanner` for the smoothest trajectory through them, and `MotionProfilePlanner` for jerk-limited point-to-point moves with multi-axis synchronization.
- [Mapping](https://github.com/kmolan/multicalc-rust/blob/main/crates/multicalc/tutorials/mapping.md): 2D `OccupancyGrid` and `ScanGeometry`

@kmolan kmolan Sep 4, 2026

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.

There are two "mapping" entries here. Likely a mistake during merge.

edit: I am seeing similar in other readme in this PR with same mistake

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

removed and checked properly.

/// assert_eq!(output.forward(hidden.view()).unwrap().into_array(), [4.0]);
/// ```
#[inline]
pub fn forward(

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.

is it even possible for this to return an Error? Looks like it always succeeds.

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.

edit: Here and in other functions in the Pr, consider applying #[must_use] wherever possible.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Result<Vector<OUTPUT, T>, LinalgError> now is just Vector<OUTPUT, T>: the approuch was to use conditional iterators methods with a branch that leads to zero but is never used, then the compiler will remove it.
applying #[must_use] : these function alredy has derived must_use and clippy warns about the redundance, then the flag -D warnings turns into CI failure, so it will stay the same.

pub fn apply<T: Numeric>(self, value: T) -> T {
match self {
Activation::Relu => {
if value > T::ZERO {

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.

what happens if value is NaN? Can it ever be NaN? What is our NaN handling strategy for this module?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

did some alterations on its handled:
/// assert_eq!(layer.forward_checked(spoiled.view()), Err(LinalgError::NonFinite));
/// // Unchecked, the rectifier turns that NaN into an ordinary 0.0.
/// assert_eq!(layer.forward(spoiled.view()).into_array(), [0.0]);

Comment thread README.md Outdated
- [Collision checking](https://github.com/kmolan/multicalc-rust/blob/main/crates/multicalc/tutorials/kinematics.md#collision-checking): `CollisionQuery` for sphere/capsule proximity — primitives on tree frames against each other and against world-fixed obstacles, with pair exclusions and fixed capacities.
- [Motion](https://github.com/kmolan/multicalc-rust/blob/main/crates/multicalc/tutorials/motion.md): `PolylinePath` for waypoint paths with arc-length, closest-point, and lookahead queries, `MinimumSnapPlanner` for the smoothest trajectory through them, and `MotionProfilePlanner` for jerk-limited point-to-point moves with multi-axis synchronization.
- [Mapping](https://github.com/kmolan/multicalc-rust/blob/main/crates/multicalc/tutorials/mapping.md): 2D `OccupancyGrid` and `ScanGeometry`
- [MLP inference](https://github.com/kmolan/multicalc-rust/blob/main/crates/multicalc/tutorials/mlp-inference.md): `Layer` and `Activation` run a trained multi-layer-perceptron policy forward on the robot — the weights and biases stay in flash behind a `MatrixView` / `VectorView` and are never copied onto the stack, and layer widths are const parameters, so a mismatched chain is a build error.

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.

`Layer` and `Activation` run a trained MLP policy forward over borrowed `MatrixView`/`VectorView` parameters, in a const no-copy no-alloc environment.

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.

make sure to also change the other readme

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

done.

@Thiago316316
Thiago316316 requested a review from kmolan September 5, 2026 01:02
@Thiago316316

Copy link
Copy Markdown
Contributor Author

Also: test (1.85) is required by branch protection, but no job produces that name any more, #339 renamed the matrix entry to test (1.92) along with the MSRV. It sits at "Expected — waiting for status to be reported" indefinitely, on every PR into main. Fix is Settings → Branches → main → required status checks: drop test (1.85), add test (1.92).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants