Skip to content

Conversation

@ana-pantilie
Copy link
Contributor

@ana-pantilie ana-pantilie commented Nov 19, 2025

@ana-pantilie

This comment was marked as outdated.

@ana-pantilie

This comment was marked as outdated.

@ana-pantilie ana-pantilie changed the title Value costing: insertCoin, unionValue Value costing: insertCoin, unionValue, scaleValue Nov 20, 2025
@ana-pantilie

This comment was marked as outdated.

@ana-pantilie ana-pantilie force-pushed the ana/value-costing-final branch 3 times, most recently from 1e44958 to abdc6ed Compare December 5, 2025 14:15
@ana-pantilie ana-pantilie marked this pull request as ready for review December 18, 2025 19:09
@github-actions
Copy link
Contributor

github-actions bot commented Dec 18, 2025

PR Preview Action v1.6.3

🚀 View preview at
https://IntersectMBO.github.io/plutus/pr-preview/cost-models/pr-7435/

Built to branch gh-pages at 2025-12-24 12:16 UTC.
Preview will be ready when the GitHub Pages deployment is complete.

"memory": {
"arguments": {
"intercept": 24,
"slope": 21
Copy link
Member

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?

Copy link
Contributor Author

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
Copy link
Member

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.

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 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
Copy link
Member

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.

Copy link
Contributor Author

@ana-pantilie ana-pantilie Dec 23, 2025

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.

Copy link
Member

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.
Copy link
Member

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"
Copy link
Member

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.

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 don't know, I haven't had time to check this yet.

Copy link
Member

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.

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 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.

Copy link
Member

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
Copy link
Member

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.

Copy link
Contributor Author

@ana-pantilie ana-pantilie Dec 23, 2025

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.

Copy link
Member

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.

Copy link
Member

@zliu41 zliu41 left a 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
Copy link
Member

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
Copy link
Member

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"
Copy link
Member

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.

Copy link
Member

@zliu41 zliu41 left a 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"
Copy link
Member

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.

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.

3 participants