-
Notifications
You must be signed in to change notification settings - Fork 502
Value costing: insertCoin, unionValue, scaleValue #7435
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
base: master
Are you sure you want to change the base?
Conversation
This comment was marked as outdated.
This comment was marked as outdated.
This comment was marked as outdated.
This comment was marked as outdated.
This comment was marked as outdated.
This comment was marked as outdated.
1e44958 to
abdc6ed
Compare
plutus-core/plutus-core/src/PlutusCore/Evaluation/Machine/CostingFun/SimpleJSON.hs
Outdated
Show resolved
Hide resolved
plutus-core/cost-model/create-cost-model/BuiltinMemoryModels.hs
Outdated
Show resolved
Hide resolved
|
| "memory": { | ||
| "arguments": { | ||
| "intercept": 24, | ||
| "slope": 21 |
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.
How are the slope and intercept determined?
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.
See the note.
| generateConstrainedValueWithMaxPolicyAndQuantity numPolicies tokensPerPolicy maxBound | ||
|
|
||
| -- | Generate constrained Value with information about max-size policy and quantity | ||
| generateConstrainedValueWithMaxPolicyAndQuantity |
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.
Did you intend to use this somewhere, perhaps in scaleValueArgs? Otherwise there's not much point factoring this out.
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.
I think this is a remnant of the experimentation which happened on this branch. I'll revert the change it if I have time.
| - 2 words for the key 'Int' closure | ||
| - 2 words for the value 'Int' closure | ||
| Putting this all together, we arrive at the following memory usage function: | ||
| 1*n + 4 + 7 + 1 |
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.
It's unclear what the "4", "7" and "1" refer to. Please make it more obvious.
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.
They come from me drawing out the closures from my understanding of ghc's memory allocation and counting the number of words (which are mentioned in the note). I don't think this methodology is the best, so we might end up removing this note in the end.
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.
The note is useful, just organize it better and make it easier to follow. There's a lot of "1 word for this", "2 words for that", etc etc., and it is non-obvious what the "4" "7" "1" correspond to.
| ... and the allocations for the 'IntMap' and nested 'Map' themselves. | ||
| The 'Map' and 'IntMap' are implemented as a self-balancing binary tree and a big-endian patricia tree, | ||
| respectively. Since we are interested in the worst-case memory usage, we assume that | ||
| for each application of the builtin function both of the trees have to be completely reallocated. |
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 only applies to unionValue, since in insertCoin and scaleValue, there's only one tree.
Also, insertCoin never completely allocates the whole tree, even in the worst case.
| "intercept": 45, | ||
| "slope": 21 | ||
| }, | ||
| "type": "linear_in_w" |
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.
I'd still like to know if insertCoin is CPU bound or memory bound.
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.
I don't know, I haven't had time to check this yet.
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.
It's important to know this because if it exhausts memory 10 times faster than it exhausts CPU, it indicates that the memory cost is nonsensical and can severely limit the usefulness of Value.
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.
I had time today to do this, these are the results. I created and ran the following test:
compiledInsertCoin :: CompiledCode BuiltinValue
compiledInsertCoin =
$$(compile [||insertCoin||])
`unsafeApplyCode` liftCodeDef "foo"
`unsafeApplyCode` liftCodeDef "bar"
`unsafeApplyCode` liftCodeDef 10
`unsafeApplyCode` liftCodeDef (mkValueInData 1000000)
mkValueInData :: Int -> BuiltinValue
mkValueInData n =
let currSymbols = (\x -> B.mkB $ "cS" <> B.encodeUtf8 (B.toBuiltin (Text.pack (show x)))) <$> take n [1 ..]
tokN = B.mkB "tN"
entries =
B.mkMap
[ ( cS
, B.mkMap [(tokN, B.mkI 100)]
)
| cS <- currSymbols
]
in B.unsafeDataAsValue entries
The resulting budget is:
CPU: 935_697
Memory: 1_786
AST Size: 12
Flat Size: 17_888_928
I would say that's definitely not memory bound, unless I didn't construct the example properly.
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.
You can do a rough calculation like this: when you insertCoin into a Value of max depth 10:
CPU = (18413 * 10 + 356924) / 10000000000
= 0.00541054% of the transaction CPU limit
Memory = (21 * 10 + 45) / 14000000
= 0.001821428% of the transaction memory limit
So we are good.
| respectively. Since we are interested in the worst-case memory usage, we assume that | ||
| for each application of the builtin function both of the trees have to be completely reallocated. | ||
| Since we are dealing with a nested 'Map' with the structure 'Map CurrencySymbol (Map TokenName Integer)', | ||
| we can consider only the worst-case scenario where the map is flattened, i.e., each currency symbol maps |
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.
I don't know why this is the worst case memory-wise, and I highly doubt this is the worst case for insertCoin. When inserting a node into a Map, usually the deeper the Map is, the more allocation is needed. I imagine a deep Map also makes rebalancing trickier.
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.
I don't know why this is the worst case memory-wise, and I highly doubt this is the worst case for insertCoin.
It's not, I was mistaken. @kwxm, @Unisay and I had a meeting a few days ago where we discussed this. I think the structure of the map doesn't matter when it comes to how much memory is allocated (for one map), only the number of nodes does. But that doesn't really matter much because we care about the new memory being allocated after the builtin run, not the total memory.
I imagine a deep Map also makes rebalancing trickier.
You're probably right, but it's tricky to figure out what the worst case is and how much memory is allocated.
I think we need to approach memory costing for the Value builtins similar to the way we approach cpu costing: by experimentally finding the worst case and generating the data which we can base predictions on. I don't think we currently do this for the memory costing of any of the other builtins, so we need to also add the required code infrastructure to generate the models using R.
At the same time, @kwxm said we gave up on doing accurate memory costing a while ago, but I don't know the reason behind this.
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.
It is almost always sufficient to determine the amount of allocation by understanding how the algorithm works. I don't even know how you can accurately measure allocations from a specific builtin call - seems quite tricky.
For insertCoin, the key is to be aware that rebalancing allocates at most O(logn) nodes. I think it's more than enough to assume that it allocates ValueMaxDepth number of nodes and be done with it. If you want to be more precise, feel free to read the paper to determine the exact worst case (which should still be far easier than trying to measure it accurately), but for memory cost we don't need to be that precise.
zliu41
left a comment
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.
The Value builtins should now be added to PlutusLedgerApi.Common.Versions.
| - 2 words for the key 'Int' closure | ||
| - 2 words for the value 'Int' closure | ||
| Putting this all together, we arrive at the following memory usage function: | ||
| 1*n + 4 + 7 + 1 |
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.
The note is useful, just organize it better and make it easier to follow. There's a lot of "1 word for this", "2 words for that", etc etc., and it is non-obvious what the "4" "7" "1" correspond to.
| respectively. Since we are interested in the worst-case memory usage, we assume that | ||
| for each application of the builtin function both of the trees have to be completely reallocated. | ||
| Since we are dealing with a nested 'Map' with the structure 'Map CurrencySymbol (Map TokenName Integer)', | ||
| we can consider only the worst-case scenario where the map is flattened, i.e., each currency symbol maps |
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.
It is almost always sufficient to determine the amount of allocation by understanding how the algorithm works. I don't even know how you can accurately measure allocations from a specific builtin call - seems quite tricky.
For insertCoin, the key is to be aware that rebalancing allocates at most O(logn) nodes. I think it's more than enough to assume that it allocates ValueMaxDepth number of nodes and be done with it. If you want to be more precise, feel free to read the paper to determine the exact worst case (which should still be far easier than trying to measure it accurately), but for memory cost we don't need to be that precise.
| "intercept": 45, | ||
| "slope": 21 | ||
| }, | ||
| "type": "linear_in_w" |
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.
It's important to know this because if it exhausts memory 10 times faster than it exhausts CPU, it indicates that the memory cost is nonsensical and can severely limit the usefulness of Value.
zliu41
left a comment
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.
Looks good but we need @kwxm to sign off on this.
| "intercept": 45, | ||
| "slope": 21 | ||
| }, | ||
| "type": "linear_in_w" |
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.
You can do a rough calculation like this: when you insertCoin into a Value of max depth 10:
CPU = (18413 * 10 + 356924) / 10000000000
= 0.00541054% of the transaction CPU limit
Memory = (21 * 10 + 45) / 14000000
= 0.001821428% of the transaction memory limit
So we are good.
Fixes https://github.com/IntersectMBO/plutus-private/issues/1899
Fixes https://github.com/IntersectMBO/plutus-private/issues/1900
Fixes https://github.com/IntersectMBO/plutus-private/issues/2004
See https://plutus.cardano.intersectmbo.org/pr-preview/cost-models/pr-7435/ for cost model vizualizations