-
Notifications
You must be signed in to change notification settings - Fork 280
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
Change "engine" to run on autodiff types only #370
Conversation
Hey @wodeni, requesting a review as you wrote the original code. It works on the examples so I'm pretty confident, but can you give it a once-over by Monday so I can merge into |
I'll go over the changes soon. Quick q about indentation: did you just change from 2 to 4 spaces? If so, I'd love to hear the rationale. Where's this README you were referring to? |
Yeah, 2 to 4 spaces, because it's what makes my editor auto-indent work correctly on both my machines... (not very principled!). I'm just referring to the PR README here. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah, 2 to 4 spaces, because it's what makes my editor auto-indent work correctly on both my machines... (not very principled!). I'm just referring to the PR README here.
I'd encourage you to try getting 2-space indentations to work in your editor just to avoid large diffs in the future. The rest of us seem to use linters/beautifiers that have this setting by default. If you have a strong preference, we can discuss that and put a rule in the linter config.
@wodeni Sure, I'll try to get the 2-space indent to work. In the meantime, can you let me know if you were planning to review this PR further? Or did you want me to fix the indent first? |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Overall okay with the changes. I'm still a little unsure about the exact amount of "duplicated code" that this PR is supposed to remove, as the original autodiff
flag affects only a few functions. If the extra conversion doesn't cost us much, I definitely would go with this cleaner version, though.
// https://stackoverflow.com/questions/31084619/map-a-javascript-es6-map | ||
// Basically Haskell's mapByValue (?) | ||
export function mapMap(map: Map<any, any>, fn: any) { | ||
return new Map(Array.from(map, ([key, value]) => [key, fn(value, key, map)])); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
My experience with Map
hasn't been great if I don't really add or remove entries and want to change existing entries a lot... You can probably use mapValues
and regular js objects if that's easier.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Got it. mapMap
isn't currently used anywhere, so I'll just leave it in as a util.
|
||
/** | ||
* Find the value of a property in a list of fully evaluated shapes. | ||
* @param shapes a list of shapes | ||
* @param path a path to a property value in one of the shapes | ||
*/ | ||
// the `any` is to accomodate `collectLabels` storing updated property values in a new property that's not in the type system |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We should probably fix that
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes...
import { Tensor, scalar } from "@tensorflow/tfjs"; | ||
import { mapValues } from 'lodash'; | ||
|
||
// TODO: Is there a way to write these mapping/conversion functions with less boilerplate? |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
High-level question: if everything is autodiff format, why do we have to do this deep conversion of the translation? For rendering purposes, can't we output shapes with autofloats and do the conversion on shapes instead?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah, true... I liked that the Shape type enforced that it only contains Value<number>
, so there is no chance an autofloat makes it to the frontend. But it's probably faster to render if we only convert shapes. Maybe I'll make this change in the future?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah the timing is up to you. If it's too involved, let's make sure we come back to it. Seems like this is even more code duplication than before :(
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This code is pretty generic, so can be used for mapping over any translation stuff, not specific to autofloat/floats. However I am down to use the shape conversion approach instead, later.
(The code duplication problem earlier involved computations + operations in evaluations needing to be defined on both autofloats and floats, e.g. adding two numbers had to use both tf.add
and the js +
depending on the inputs, and it was difficult for me to debug why autofloats/floats ended up where they weren't supposed to be.)
react-renderer/src/Evaluator.ts
Outdated
const props = mapValues(propExprs, (prop: TagExpr<Tensor>): Value<number> => { | ||
if (prop.tag === "OptEval") { | ||
// For display, evaluate expressions with autodiff types (incl. varying vars as AD types), then convert to numbers | ||
// (The tradeoff for using autodiff types is that evaluating the display step will be a little slower, but then we won't have to write two versions of all computations) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Will need to see profiling results to see the overhead of the Tensor
=> number
step, otherwise okay with me.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
react-renderer/src/Optimizer.ts
Outdated
const { energy } = minimize(f, fgrad, xs, steps); | ||
// insert the resulting variables back into the translation for rendering | ||
// NOTE: this is a synchronous operation on all varying values; may block | ||
// Note: after we finish one evaluation, we "destroy" the AD variables (by making them into scalars) and remake them |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What's the consequence of not "destroying" the AD variables?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If you reuse an AD variable later with our dynamic evaluator approach, then my guess is that Tensorflow records those additional operations in its computational graph, which might affect the gradient. But it's a moot point after we convert to using our custom AD + compiled gradients, because the AD vars become irrelevant after the gradient is compiled.
9db66e6
to
2725bc0
Compare
@@ -4,7 +4,7 @@ | |||
"outDir": "build/dist", | |||
"module": "esnext", | |||
"target": "es5", | |||
"lib": ["es5", "es6", "es7", "dom"], | |||
"lib": ["es5", "es6", "es7", "esnext", "dom"], |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
FYI I added esnext here @wodeni @maxkrieger. Wanted to use Object.fromEntries
in EngineUtils
. Lmk if unnecessary
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Lgtm
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Not sure how the polyfill coverage is but shouldn't be a problem
…s and types to be on autodiff types only (in this case, Tensors). remove autodiff=true flag from evaluator. convert to and from the `number` type for display. factor out computations into a separate file, and convert their types to be autodiff as well
c9db040
to
4963b0e
Compare
FYI @wodeni I just made changes WRT the above suggestions and reverted the indentation to 2 spaces, and squashed/force-pushed to this branch. Diff should make more sense now. (Also added |
Thanks! BTW GitHub does give you an option to ignore whitespace changes, so the diff was no issue for me :D |
Description
Previously, the "engine" had the option of using
number
orTensor
types in the evaluator via theautodiff
flag, because we needed the former type for displaying shapes (evaluating the translation) and the latter type for optimization (evaluating the energy).While concise, the design started to cause some challenges:
number
s in the optimization)Therefore, this PR changes all evaluation + optimization-related functions and types to be on autodiff types only (in this case, Tensors).
The tradeoff is that the display step may take slightly longer and use more space, since it requires doing all work on autodiff types (which are objects that store a good amount of information), then converting them to numbers. However, that tradeoff is acceptable because the engine does much more optimization-type evaluation than display-type evaluation.
Implementation strategy and design decisions
The new pipeline is as follows:
JSON
, which gives us numbersValue<number>
intoValue<Tensor>
to store in theTranslation
, which now only holdsTensors
varyingValues
are also converted to be allTensor
s, as are the pending values (label dimensions) after they are calculatedOptEval
) that can be evaluated at optimization-time are not converted; if anumber
is encountered in evaluation, it's converted on the spot inevalExpr
Tensors
Tensors
stored in the translationevalTranslation
, it also does all computations onTensors
, and does a final conversion pass fromValue<Tensor>
toValue<number>
In general, numeric values are converted to tf
Scalars
via thescalar
function (so they just tensors, not variables in the optimization), unless they are varying values, in which case they are converted to tfVariable
s.Most changes are in
Evaluator
andtypes.d.ts
, with some changes inPropagateUpdate
andCanvas
, and new filesEngineUtils
andComputations
.Also, the diff is a little misleading because I changed my editor's indentation, but I kept it because I will probably use this indentation in the future.[0] If something is deserialized with the wrong type (e.g. the deserializer doesn't know that something is supposed to be a
Tensor
, not anumber
), our types are gone at runtime, so there are no guarantees and the wrong type just gets everywhere.Examples with steps to reproduce them
Tensor
s:runpenrose hyperbolic-domain/hyperbolic-example.sub hyperbolic-domain/PoincareDisk.sty hyperbolic-domain/hyperbolic.dsl
The following three are successfully-repro'ed examples from the wiki page for
web-runtime
working examples.Test labels, pending values, varying values, and optimization:
runpenrose set-theory-domain/tree.sub set-theory-domain/venn-opt-test.sty set-theory-domain/setTheory.dsl
runpenrose set-theory-domain/tree.sub set-theory-domain/tree.sty set-theory-domain/setTheory.dsl
runpenrose set-theory-domain/continuousmap.sub set-theory-domain/continuousmap.sty set-theory-domain/setTheory.dsl
Checklist
stack test
Open questions
Questions that require more discussion or to be addressed in future development:
Tensor
s). UsingTensor
s is a stopgap untilweb-perf
and @strout18's macro code get merged.Variable
s needed to have their gradients cleared after each step, so for safety, I did that inOptimizer
at the end of eachstepEP
. In general, when we use custom AD vars, we may need to clear them.Scalar
s andVariable
. Hopefully this generates the right computational graph when we use custom AD vars.