What a ResourceModel is
An object that answers one question about one module:
Given this elaborated component, what does it alone cost, and how much should I believe that?
“It alone” is load-bearing: a model never accounts for its children — that is composition’s job.
You will probably not implement any of this. The methods below are the interface; the library supplies the model kinds that implement them, and a design just picks one in
get_rm. This page exists so the vocabulary in the rest of the section means something — each kind gets its own page.
The interface
class ResourceModel(CalibModel):
def get_params(self, comp, **runtime) -> dict: ... # component -> parameters (RECORDED)
def transform(self, params) -> dict: ... # parameters -> features
def predict(self, comp) -> dict: ...
def confidence(self, comp) -> Confidence: ...
def fit(self, samples=None) -> "ResourceModel": ...
A ResourceModel is a CalibModel whose targets are the platform’s
counters. Everything generic — the storage paths, the corpus, confidence — comes from that base;
what is resource-flavoured is the counter vocabulary and a prediction that is a mapping rather than
a scalar.
get_params(comp) — extract, and record
The default is the identity — every resolved HwParam, by name:
>>> model.get_params(comp)
{'depth': 128, 'width': 32}
Override it to reach past HwParam, for a fact that shapes the hardware but is not declared — a port
count, a channel width, a declared multiplier group. InterfaceResourceModel does exactly this, and
must: a composite’s own cost depends on its ports and channels, which no HwParam records.
Whatever it returns is the corpus row, so keep it raw.
transform(params) — derive
Takes a parameter mapping, never a component. Override it for a derived quantity:
def transform(self, params):
return {"area": params["a"] * params["b"] * params["c"]}
Choosing a transform is a modelling assertion. Collapsing three parameters into a·b·c says
“nothing but the product matters”. That may be exactly right — and if it is, one measurement covers
every configuration sharing a product. It is your claim to make; the model simply carries it.
And because it is a claim, it will be revised — so derive here and record the inputs in
get_params. A model that storedareainstead ofa,b,cstrands every measurement the moment the product turns out to be the wrong form. See the corpus.
Full detail on the split, and why transform cannot see the component, is in
What a CalibModel is.
predict(comp) — the counters
Returns {counter: value} for this module alone. Takes the component, never a feature vector —
see below.
confidence(comp) — how much to believe it
Returns a Confidence: a level plus facts explaining it.
@dataclass(frozen=True)
class Confidence:
level: ConfidenceLevel # EXACT | INTERPOLATED | EXTRAPOLATED | UNCALIBRATED
facts: dict # free-form, but must be JSON-able
facts is deliberately model-specific — only level is guaranteed. One key is conventional:
summary, a one-line human string, which Confidence.summary falls back to a generated string
for if absent. to_json() flattens the whole thing to {"level": …, **facts}, which is what an agent
or a report consumes.
A real one:
{'level': 'EXACT',
'summary': 'Blk: dsp from an analytical prior with no fitted parameters',
'module_key': 'blk-22e53744',
'model': 'prior',
'counters': ['dsp'],
'inputs': {'area': 4096}}
There is no schema to conform to: put in whatever a reader would need in order to judge the number. Each model kind documents the keys it adds.
fit(samples) — calibrate from measurements
Takes a corpus — or, on the resource side, pairs of (component, measured
counters) — and returns itself, calibrated. With no argument it reads what was measured: a model
given a store= reduces its record store on demand. What “calibrated” means depends on the kind: a lookup memorizes one row per sample; a
Vitis model regresses its fabric coefficients and derives the rest from structure.
A model with genuinely nothing to learn returns itself unchanged.
Counters — you do not define these
Three related things, and none of them is yours to write:
| what it is | who supplies it | |
|---|---|---|
| the vocabulary | which counters exist at all — lut, ff, dsp, bram, uram, srl |
the platform, because a counter set is a property of the technology |
declared_counters() |
which of them this model predicts | the model kind, automatically |
check_counters() |
refuses a counter outside the vocabulary | the base |
The platform reaches the model because it is a constructor argument on the shared base, and every kind inherits it:
@dataclass
class CalibModel:
name: str = "" # identifies the model, and names its storage directory
platform: Any = None # supplies the counter vocabulary; None -> the FPGA default
...
def counters(self) -> tuple:
return tuple(self.platform.res_types) if self.platform is not None else COUNTERS
So LookupResourceModel(store=…, platform=platform) and
VitisResourceModel(name=…, part=…, platform=platform) are both just passing that base field through.
With platform=None a model still works — it falls back to the built-in FPGA counter set, which is
what lets tests construct models with no platform at all.
declared_counters() is derived rather than written — each kind computes it from what it was given.
Why the check exists. Without it a mistyped counter predicts fine in isolation and is silently dropped when counters are summed — so the module contributes zero. A missing contribution makes a design read as cheaper than it is, which is the one direction an estimate must not err: it turns “does not fit” into “fits”.
The component-facing entries take a component, never features
predict(comp) and confidence(comp) take the elaborated module; transform(params) deliberately
cannot see it. The split is the params/transform contract — what follows is why
the outer entries are shaped this way.
Models in one hierarchy are heterogeneous. Different kinds need different things — an identity, a
set of parameters, a description of the ports. If compose had to supply the inputs,
it would have to know every kind’s requirements. get_params is where that variation lives, which is
why it takes the component.
One model must price many components. During fit the same object evaluates every
point in a corpus; during compose, every sibling in a hierarchy.
A model that closed over one instance would return that instance’s features for all of them. Every row of the fit becomes identical, the regression is rank-deficient, the coefficients are meaningless — and nothing raises.
That is why
get_rmis a classmethod: having noselfmakes the mistake impossible rather than merely discouraged.
Next
- Samples — the training data every
fittakes, and where it comes from. - Then the kinds themselves, starting with the lookup model.