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

Kwxm/costing/read model type from r #5841

Closed
wants to merge 8 commits into from

Conversation

kwxm
Copy link
Contributor

@kwxm kwxm commented Mar 17, 2024

This demonstrates how to simplify the cost modelling procedure somewhat by simplifying the interface between Haskell and the R code (it's a step towards PLT-408, amongst other things). In particular, the "shapes" of the CPU costing functions used to be hard-coded into the Haskell source despite the fact that there's a tag in the JSON file that tells you what the shape is. This now gets the R code to return a similar tag to Haskell so the during cost model generation the Haskell code can look at the tag and parse the R model objects appropriately. I'm also passing a much simpler structure from R to Haskell.

While I was at it I took the opportunity to improve a couple of other things. One noticeable change is that some functions (equalsString for example) are very fast if the argument have different sizes (since in that case they can't possibly be equal) so there's a linear costing function on the diagonal where the sizes of the two arguments are the same, and there's a constant cost off the diagonal. Previously the off-diagonal constant costs were hard-coded into the Haskell code, but now they're passed in from the R code (and could be inferred there, although I haven't gone that far yet). They're also stored in builtinCostModel.json, which introduces some new cost model parameters that the ledger would have to know about. I tidied up a couple of other things (and I'd like to do more along the same lines) which has led to the tags in the JSON file changing, and hence also the names of some of the cost model parameters.

@kwxm kwxm added the EXPERIMENT Experiments that we probably don't want to merge label Mar 17, 2024
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.

In particular, the "shapes" of the CPU costing functions used to be hard-coded into the Haskell source despite the fact that there's a tag in the JSON file that tells you what the shape is. This now gets the R code to return a similar tag to Haskell so the during cost model generation the Haskell code can look at the tag and parse the R model objects appropriately.

I'm not sure I totally understand. What exactly was hardcoded in Haskell before, that is un-hardcoded in this PR?

},
"type": "const_above_diagonal"
"type": "multiplied_sizes"
Copy link
Member

Choose a reason for hiding this comment

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

Does changing the JSON file like this not break everything? I think we need to record both the old shape (for use by PlutusV1 and V2 before PV9) and the new shape?

Copy link
Contributor Author

@kwxm kwxm Mar 18, 2024

Choose a reason for hiding this comment

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

Sorry, that was a mistake: I hadn't handled nested costing functions properly. I've fixed that now.

Copy link
Contributor Author

@kwxm kwxm Mar 18, 2024

Choose a reason for hiding this comment

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

But in principle, yes we would need multiple copies of builtinCostModel.json, one for each PlutusV<n>. That might be useful anyway because it'd let us run uplc with different cost models. Anyway, this PR is kind of exploratory.

Copy link
Contributor

Choose a reason for hiding this comment

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

builtinCostModel.json, one for each PlutusV<n>

Not just PlutusV<n>, though? We also need to worry about protocol versions. And I don't think we need to worry about all of PlutusV<n>. I think the end result is that for Conway we'll need 3 files (it's a coincidence that we have exactly that many Plutus versions). I'll write about that later.

Copy link
Member

Choose a reason for hiding this comment

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

We need one JSON per semantic variant, so 3 sounds right. Of course, we can merge them into a single JSON file, but all the information needs to be there.

Copy link
Contributor

@effectfully effectfully left a comment

Choose a reason for hiding this comment

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

I'll probably need to have another run, here's an initial review.

case ty of
"constant_cost" -> ModelOneArgumentConstantCost <$> getConstant e
"linear_cost" -> ModelOneArgumentLinearCost <$> readOneVariableLinearFunction "x_mem" e
"linear_in_x" -> ModelOneArgumentLinearCost <$> readOneVariableLinearFunction "x_mem" e -- FIXME: duplicate
Copy link
Contributor

Choose a reason for hiding this comment

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

Gonna do that in this PR?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yes. I forgot about that.

"linear_in_y" -> ModelThreeArgumentsLinearInY <$> readOneVariableLinearFunction "y_mem" e
"linear_in_z" -> ModelThreeArgumentsLinearInZ <$> readOneVariableLinearFunction "z_mem" e
"quadratic_in_z" -> ModelThreeArgumentsQuadraticInZ <$> readOneVariableQuadraticFunction "z_mem" e
"literal_in_y_or_linear_in_z" -> ModelThreeArgumentsLiteralInYOrLinearInZ <$> error "literal"
Copy link
Contributor

Choose a reason for hiding this comment

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

And this one.


boolMemModel :: ModelTwoArguments
boolMemModel = ModelTwoArgumentsConstantCost 1

memoryUsageAsCostingInteger :: ExMemoryUsage a => a -> CostingInteger
memoryUsageAsCostingInteger = coerce . sumCostStream . flattenCostRose . memoryUsage

-- | The types of functions which take an R SEXP and extract a CPU model from
-- it, then pair it up with a memory costing function.
type MakeCostingFun1 = forall m . MonadR m => SomeSEXP (Region m) -> m (CostingFun ModelOneArgument)
Copy link
Contributor

Choose a reason for hiding this comment

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

Have a single

type MakeCostingFun model = forall m . MonadR m => SomeSEXP (Region m) -> m (CostingFun model)

instead and use it as MakeCostingFun ModelTwoArguments? Even if only for defining the same type synonyms.

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 thought there would probably be a more generic way of doing this but I was in a hurry! I'll think about that again later.

Copy link
Contributor

Choose a reason for hiding this comment

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

I think doing it generically isn't gonna be any better, my version of MakeCostingFun merely eliminates some of the boilerplate.

CostingFun <$> (loadThreeVariableCostingFunction e) <*> pure memModel

makeSixVariableCostingFunction :: ModelSixArguments -> MakeCostingFun6
makeSixVariableCostingFunction memModel e =
Copy link
Contributor

Choose a reason for hiding this comment

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

I'd make all these loadOneVariableCostingFunction, loadTwoVariableCostingFunction etc into a single method of a type class taking ModelOneArgument, ModelTwoArguments etc at the type-level.

pure $ CostingFun cpuModel memModel
addInteger :: MakeCostingFun2
addInteger =
makeTwoVariableCostingFunction $ ModelTwoArgumentsMaxSize $ OneVariableLinearFunction 1 1
Copy link
Contributor

Choose a reason for hiding this comment

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

So we unhardcode parsing, but we still have to keep those models hardcoded here, because that's how we get the data and there's no other way we could infer them, right?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

That's right. If we wanted to be totally general I suppose we could return the model formulas from R (things like this) and try to parse them and then we could deal with more or less arbitrary functions (and we'd also need some language for describing regions of the input space where different cost models apply). That'd be the most generic solution to PLT-408, but it might be overkill for what we're doing.

Copy link
Contributor

Choose a reason for hiding this comment

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

Yeah, that does sound like an overkill. Thanks!

It would be really great if we could somehow have these models hardcoded here and reused within the definitions of builtins. No JSON for shapes, just taking the shapes directly from this place and using them where we need them. That would be the most reliable and ergonomic solution I think, but for that we also need to untangle shapes from constants (currently we put constants right into the shapes basically).

I feel like my approach would be better long-term. Short-term, not so sure, perhaps yours is better. Both are complicated though: with yours we need to have multiple JSON files or to add support for some form of versioning into the existing JSON file (right?) and with mine we need to rewrite half the costing code. Yours sounds annoying, mine sounds insufferable.


divideInteger :: MakeCostingFun2
divideInteger =
makeTwoVariableCostingFunction $ ModelTwoArgumentsSubtractedSizes $ ModelSubtractedSizes 0 1 1
Copy link
Contributor

Choose a reason for hiding this comment

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

Wait, but we have

    "subtracted_sizes"     -> ModelTwoArgumentsSubtractedSizes    <$> error "subtracted sizes"

?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

That's the memory usage: we don't need to parse that.

Copy link
Contributor

Choose a reason for hiding this comment

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

I'm not sure I understand, but I trust you!

Comment on lines -322 to +324
| ModelTwoArgumentsMultipliedSizes ModelMultipliedSizes
| ModelTwoArgumentsMinSize ModelMinSize
| ModelTwoArgumentsMaxSize ModelMaxSize
| ModelTwoArgumentsMultipliedSizes OneVariableLinearFunction
| ModelTwoArgumentsMinSize OneVariableLinearFunction
| ModelTwoArgumentsMaxSize OneVariableLinearFunction
Copy link
Contributor

Choose a reason for hiding this comment

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

Drop the definitions of ModelAddedSizes, ModelMultipliedSizes, ModelMinSize and ModelMaxSize then?

Copy link
Contributor Author

@kwxm kwxm Mar 18, 2024

Choose a reason for hiding this comment

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

Yes, I want to rework some of this later. It's a bit confusing because it'll change the names of some of the cost model parameters and it's kind of orthogonal to what I was trying to do (although I did get carried away and make some changes along those lines that probably shouldn't be in this PR).

@kwxm
Copy link
Contributor Author

kwxm commented Mar 18, 2024

In particular, the "shapes" of the CPU costing functions used to be hard-coded into the Haskell source despite the fact that there's a tag in the JSON file that tells you what the shape is. This now gets the R code to return a similar tag to Haskell so the during cost model generation the Haskell code can look at the tag and parse the R model objects appropriately.

I'm not sure I totally understand. What exactly was hardcoded in Haskell before, that is un-hardcoded in this PR?

This is mostly just about improving the R/Haskell interface. The Haskell code that generates the cost model from the R output currently has the "shapes" of the CPU costing functions hardcoded, like this:

 cpuModel <- ModelTwoArgumentsAddedSizes <$> readModelAddedSizes cpuModelR

It reads the numbers from R and then fits them into a ModelTwoArgumentsAddedSizes object. However, the R code is actually what decides that the model's supposed to be added sizes, like this

lm(t ~ I(x_mem + y_mem)

(R has a rather esoteric syntax for specifying regression formulae).

This PR is mostly about completely removing the hardcoded model shapes from the Haskell code and getting R to tell it the shape instead (and it moves some hardcoded numerical constants into the R code too). Now the Haskell code knows nothing at all about the shapes of CPU costing functions for specific builtins, which should make it a bit more flexible; it still has all of the memory costing functions hardcoded, but we could put them in a JSON file too.

I don't know if we actually want to do this, but I wanted to check that it was possible. It helped me to spot a few other things that could do with being tidied up too.

@kwxm
Copy link
Contributor Author

kwxm commented Mar 26, 2024

Closed in favour of #5857.

@kwxm kwxm closed this Mar 26, 2024
@kwxm kwxm deleted the kwxm/costing/read-model-type-from-R branch June 19, 2024 13:09
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
EXPERIMENT Experiments that we probably don't want to merge
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants