Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

unify compile-fn and compile-state-fn; primitive mode for compiled fns #115

Merged
merged 10 commits into from
Apr 5, 2023

Conversation

littleredcomputer
Copy link
Contributor

@littleredcomputer littleredcomputer commented Mar 13, 2023

From the CHANGELOG:

  • unify compile-fn and compile-state-fn; primitive mode for compiled fns #115:

    This PR introduces significant upgrades to the functions in
    emmy.expression.compile. compile-state-fn and compile-fn now share the
    same code. Expect some more shifts here as we work on animations.

    Specifically, this update:

    • Removes timers from the code in emmy.numerical.ode. If you want timing
      data you can generate an derivative yourself that performs timing. Including
      this by default introduced a significant performance cost.

    • Renames emmy.numerical.ode/integration-opts to make-integrator*; it now
      returns only a call to stream-integrator instead of the old map of this
      result and timers.

    • In emmy.expression.compile:

      • compile-state-fn* and compile-fn* are now removed, in favor of the
        non-starred versions with option :cache? false supplied.

      • compile-fn now calls compile-state-fn with params set to false and
        an :arity option supplied, vs using its own mostly-duplicated
        implementation.

      • :flatten? option removed in favor of the more granular
        :calling-convention. This option supports values of :structure,
        :flat and :primitive. See the docstring for details.

      • :mode supports :js, :source, :clj, :native or :sci.

Redesign compiler as a pipeline of transformations that works by
having each step add or update keys in a map
representing the current state of the compilation,
avoiding the need for bookkeeping parameters.
@littleredcomputer littleredcomputer marked this pull request as draft March 13, 2023 17:41
@codecov
Copy link

codecov bot commented Mar 13, 2023

Codecov Report

Patch coverage: 87.09% and project coverage change: -0.03 ⚠️

Comparison is base (7631909) 86.45% compared to head (9f45424) 86.43%.

❗ Current head 9f45424 differs from pull request most recent head b98fef5. Consider uploading reports for the commit b98fef5 to get more accurate results

Additional details and impacted files
@@            Coverage Diff             @@
##             main     #115      +/-   ##
==========================================
- Coverage   86.45%   86.43%   -0.03%     
==========================================
  Files          99       99              
  Lines       15306    15353      +47     
  Branches      785      798      +13     
==========================================
+ Hits        13233    13270      +37     
+ Misses       1288     1285       -3     
- Partials      785      798      +13     
Impacted Files Coverage Δ
src/emmy/expression/cse.cljc 97.29% <ø> (ø)
src/emmy/expression/compile.cljc 84.77% <86.20%> (-2.99%) ⬇️
src/emmy/numerical/ode.cljc 91.80% <100.00%> (ø)

... and 1 file with indirect coverage changes

Help us with your feedback. Take ten seconds to tell us how you rate us. Have a feature suggestion? Share it here.

☔ View full report in Codecov by Sentry.
📢 Do you have feedback about the report comment? Let us know in this issue.

- redo cache to include relevant information in cache key
- this makes the `*` and non-`*` versions of compile unnecessary
- You can supply the option `{:cache false}` if you really want to
- non-generic-params compilations are excluded from caching
- primitive calling convention now "works", but not yet wired into ODE
@littleredcomputer
Copy link
Contributor Author

@sritchie once asked "why not just use Clojure as the representation of code and translate everything to JS at the last minute" and this PR is proof that that was a good idea. render/->JavaScript still works at the expression level only, but we have a zipper based JS rewriter that uses this lower-level function to write the function body with CSE and primitive state/param extraction and array-based return value. This gets wired up into the ODE solver since we now compile to a function just like what the solvers want and the results are promising.

@littleredcomputer littleredcomputer marked this pull request as ready for review March 16, 2023 23:29
@sritchie
Copy link
Member

Amazing, I will review this tomorrow and kick the tires!

Copy link
Member

@sritchie sritchie left a comment

Choose a reason for hiding this comment

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

This is all looking really, really good... few comments at the edges, but this looks like it's ready already for some performance testing from the animations.

src/emmy/expression/cse.cljc Show resolved Hide resolved
@@ -173,7 +173,7 @@



(defn integration-opts
(defn- integration-opts
Copy link
Member

Choose a reason for hiding this comment

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

If you don't mind, can you make this (defn ^:no-doc ...) instead of private? I think I need this from some of the simulation code I was writing, though that may not be true anymore. That metadata will keep the var out of public docs, but not actually keep the var private.

Your call, I can always use #' to get the var.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I do not know why I changed this: maybe a prompt from the linter but all seems well with :no-doc

(let [f' (c/compile-state-fn state-derivative derivative-args initial-state)]
(fn [y]
(f' y (or derivative-args []))))
(c/compile-state-fn state-derivative derivative-args initial-state
Copy link
Member

Choose a reason for hiding this comment

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

one mode I would like to see, since it matters for the animation code, is "I am already giving you something compiled with a primitive calling convention, please just use it as is."

This comes up because we send source over the wire to Clerk in the browser, and it felt right to have Reagent trigger a final eval step every time new source shows up, but then pass it in directly to the ODE solver powering the animation.

Copy link
Member

Choose a reason for hiding this comment

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

That would also let us delete the timing code below, since the user could compile the function themselves and then call an instrumented combinator if they wanted to use it, and hang on to their own stopwatch reference.

equations (fn [_ y out]
;; TODO: should we consider allowing an option to add a dummy
Copy link
Member

Choose a reason for hiding this comment

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

Removing this timer code made a significant difference in performance for the animations. I would recommend that we remove it here...

See my comment above for a possible workaround. We could also take an :instrument? flag, of course. Thoughts?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

The double-decker state derivative is annoying to deal with in this case since (in the SICM-book-compatible cases) we want to instrument the inner function that is returned from the application to derivative-args. So I'm going to take it out for now--as you say the customer can arrange for it themselves

(us/stop evaluation-time))
(derivative-fn y out primitive-params)
(us/stop evaluation-time)
(swap! evaluation-count inc))

integrator (stream-integrator equations 0 flat-initial-state opts)]
{:integrator integrator
Copy link
Member

Choose a reason for hiding this comment

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

if we didn't instrument by default, and forced the customer to do it, we could just return the integrator here, which feels cleaner. Maybe that's a clue that this is the right move. What do you think?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I think this conclusion is correct.

src/emmy/expression/compile.cljc Outdated Show resolved Hide resolved
~body))))
- `:body`: a function body making use of any symbol in argv above"
[{:keys [argv body]}]
(w/postwalk-replace sym->resolved-form `(fn [~@argv] ~body)))
Copy link
Member

Choose a reason for hiding this comment

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

sym->resolved-form smells like the next keyword option, probably for another PR. Some other dictionaries of math that I would use:

its code object argument in a form suitable for application of the Function
constructor."
[{:keys [argv body]}]
(let [argv (mapv commafy-arglist argv)
Copy link
Member

Choose a reason for hiding this comment

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

nice, so this gets all of the bindings up at the top.

current runtime environment."
[code]
#?(:clj (eval (compile->clj code))
:cljs (comp js->clj (apply js/Function (compile->js code)))))
Copy link
Member

Choose a reason for hiding this comment

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

do we still need this js->clj in every case? Not in the primitive calling convention case, right?

compiled))))))
(compile-fn f n {}))
([f n opts]
(let [argv (into [] (repeatedly n #(gensym 'x)))]
Copy link
Member

Choose a reason for hiding this comment

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

If we make parameters a keyword option, do you think we could merge compile-fn and compile-state-fn into a single thing? compile-state-fn would be a compile-fn call with parameters supplied, otherwise the function's treated as one level deep?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Currently I delegated it the other way, but it could be reversed (and I agree that direction of delegation makes more sense conceptually) but "state function nature" has a large footprint. Let me think a little more about that

@sritchie
Copy link
Member

small request after staring at the animation code... could we add a continuation return type, that lets the user pass in an emit function which should receive all of the arguments that the native mode sets on an output vector?

@littleredcomputer
Copy link
Contributor Author

on the matter of emit: the integrator needs the values too. Is the animation code watching function evaluations of f? They points at which the function is evaluated are chosen by the integrator for reasons of its own. We do not make any guarantees about the order or granularity of such invocations.

Another way to handle it would be to wrap the compiled function with a dispatcher.

Maybe that is the way to handle instrumentation as well. You could pass an option, perhaps called finalize or adapt, an "operator" which would be applied to the compiled function before it was given to the integrator. The contract would be that the operator not do something that would "complicate the caching" (or else you can specify cache?: false if you really want to do evil).

With that idea, you could instrument and emit or even pass the composition of the two combinators to make-integrator*.

@sritchie
Copy link
Member

@littleredcomputer I grew deeply confused when I read your comment... why was emit ever working for me when I wired it in??

After a desperate minute I remembered that my emit uses were separate from the ODE. emit mode was important for visualizing a manifold, or any place where I was passing some code directly to mathbox so that it could graph out a surface. Mathbox handed me pairs of (theta, phi), and I emitted x,y,z coordinates directly. So rather than creating an output array, having the compiled function mutate it and then calling (emit (aget ...) (aget ...) or (apply emit ...), passing in emit directly was the most efficient thing to do.

- avoid js->clj in primitive case
- remove timing instrumentation in ODE fns
- `integration-opts` is now `make-integrator*` & is simpler
@littleredcomputer
Copy link
Contributor Author

Understood. Then let's have both :calling-convention (unmodified) and :return-convention which can be :primitive which gets you doto out (aset ... or "something for which fn? returns true" which gives you spread application to the values (k v1 v2 ...).

The ODE code will select :primitive for both, or :structure, if :compile is falsy. You'd select :primitive for :calling-convention and emit for :return-convention.

@sritchie
Copy link
Member

That sounds great, and I think we now have all of my animation use cases covered. Just in time, since it looks like there will be at least one other talk that's using the animation libraries to go over some physics and math.

@littleredcomputer
Copy link
Contributor Author

Understood. Then let's have both :calling-convention (unmodified) and :return-convention which can be :primitive which gets you doto out (aset ... or "something for which fn? returns true" which gives you spread application to the values (k v1 v2 ...).

The ODE code will select :primitive for both, or :structure, if :compile is falsy. You'd select :primitive for :calling-convention and emit for :return-convention.

This is on hold because it's not obvious how to paste up a call to an arbitrary function inside our "compiler". One way to do it would just be to comp with the result of a primitive computation, and take the hit of (apply f out) instead of forming a function call with the arguments spread out.

@sritchie sritchie merged commit ac6e444 into main Apr 5, 2023
@sritchie sritchie deleted the littleredcomputer/compiler-2 branch April 5, 2023 14:01
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