Skip to content

Commit cee66aa

Browse files
committed
Merge orchestrator/optim (resolve emit_blob.py rule/cond size conflict)
2 parents 94b0942 + d0e6d16 commit cee66aa

8 files changed

Lines changed: 243 additions & 4 deletions

File tree

include/arbiter/arbiter.h

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -144,6 +144,23 @@ int ARBITER_fault_is_raised(const struct ARBITER_result *result,
144144
int ARBITER_get_requested_actions(const struct ARBITER_result *result,
145145
const uint16_t **actions, size_t *count);
146146

147+
/**
148+
* @brief Set multiple fact values in a single call.
149+
*
150+
* More efficient than calling ARBITER_set_i32() N times because
151+
* context validation is performed once.
152+
*
153+
* @param ctx Initialized context.
154+
* @param fact_ids Array of fact indices.
155+
* @param values Array of int32_t values.
156+
* @param count Number of elements.
157+
* @return ARBITER_OK on success, or the first error encountered.
158+
*/
159+
int ARBITER_set_facts(struct ARBITER_ctx *ctx,
160+
const uint16_t *fact_ids,
161+
const int32_t *values,
162+
uint16_t count);
163+
147164
/**
148165
* @brief Get the operation count from the last evaluation.
149166
*/

include/arbiter/arbiter_model.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -154,6 +154,7 @@ struct ARBITER_rule_def {
154154
arbiter_index_t expr_count; /**< Number of expressions to evaluate. */
155155
arbiter_index_t safety_goal_id;
156156
arbiter_index_t set_mode;
157+
arbiter_index_t required_mode; /**< Mode required to evaluate (INDEX_MAX = any). */
157158
bool safety_critical;
158159
#if !defined(CONFIG_ARBITER_STRINGS) || CONFIG_ARBITER_STRINGS
159160
const char *name;

include/arbiter/arbiter_result.h

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,9 @@ struct ARBITER_snapshot {
2929
uint16_t count;
3030
uint32_t timestamp_ms;
3131
bool frozen;
32+
#if defined(CONFIG_ARBITER_DIRTY_SKIP) && CONFIG_ARBITER_DIRTY_SKIP
33+
uint64_t dirty_mask; /**< Bitmask of facts changed since last eval. */
34+
#endif
3235
};
3336

3437
/** Evaluation result. */

lib/arbiter_eval.c

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,20 @@ ARBITER_ALWAYS_INLINE int32_t resolve_operand(
6262
return 0;
6363
}
6464

65+
/* ── Condition result cache ────────────────────────────────────── */
66+
67+
#if defined(CONFIG_ARBITER_COND_CACHE) && CONFIG_ARBITER_COND_CACHE
68+
#define ARBITER_COND_CACHE_SIZE 8
69+
70+
struct arbiter_cond_cache_entry {
71+
uint16_t fact_id;
72+
uint8_t op;
73+
int32_t value;
74+
bool result;
75+
bool valid;
76+
};
77+
#endif /* CONFIG_ARBITER_COND_CACHE */
78+
6579
/* ── Condition evaluator ──────────────────────────────────────── */
6680

6781
/**
@@ -403,9 +417,61 @@ int ARBITER_eval(const struct ARBITER_model *model,
403417
*/
404418
uint32_t ops = 0;
405419

420+
#if defined(CONFIG_ARBITER_COND_CACHE) && CONFIG_ARBITER_COND_CACHE
421+
/* Per-eval condition result cache — reset each cycle. */
422+
struct arbiter_cond_cache_entry cond_cache[ARBITER_COND_CACHE_SIZE];
423+
424+
memset(cond_cache, 0, sizeof(cond_cache));
425+
#endif
426+
427+
#if defined(CONFIG_ARBITER_DIRTY_SKIP) && CONFIG_ARBITER_DIRTY_SKIP
428+
const uint64_t dirty_mask = snapshot->dirty_mask;
429+
#endif
430+
406431
for (arbiter_index_t r = 0; r < rule_count; r++) {
407432
const struct ARBITER_rule_def *__restrict rule = &rules[r];
408433

434+
/* ── Mode-aware pruning ──────────────────────── */
435+
if (rule->required_mode != ARBITER_INDEX_MAX &&
436+
rule->required_mode != result->current_mode) {
437+
ops += 1u;
438+
continue;
439+
}
440+
441+
#if defined(CONFIG_ARBITER_DIRTY_SKIP) && CONFIG_ARBITER_DIRTY_SKIP
442+
/* ── Dirty-flag rule skip ────────────────────── */
443+
/*
444+
* If none of the rule's input facts have changed,
445+
* skip re-evaluation (keep previous result).
446+
* For rules with set_mode or no conditions, always
447+
* evaluate (dep_mask == 0 means unconditional).
448+
*/
449+
if (rule->condition_count > 0) {
450+
uint64_t dep_mask = 0;
451+
const arbiter_index_t cs = rule->condition_start;
452+
const arbiter_index_t cc = rule->condition_count;
453+
454+
for (arbiter_index_t ci = 0; ci < cc; ci++) {
455+
const arbiter_index_t idx = cs + ci;
456+
457+
if (likely(idx < cond_table_count)) {
458+
const arbiter_index_t fid =
459+
conds[idx].fact_id;
460+
461+
if (fid < 64) {
462+
dep_mask |=
463+
((uint64_t)1 << fid);
464+
}
465+
}
466+
}
467+
if (dep_mask != 0 &&
468+
(dirty_mask & dep_mask) == 0) {
469+
ops += 1u;
470+
continue;
471+
}
472+
}
473+
#endif /* CONFIG_ARBITER_DIRTY_SKIP */
474+
409475
/* ── Conditions ──────────────────────────────── */
410476
const bool fired = eval_condition_group(
411477
conds, values, vcount, snap_ts,

lib/arbiter_fact_store.c

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,38 @@ int ARBITER_set_timestamp(struct ARBITER_ctx *ctx, uint16_t fact_id,
9696
return ARBITER_OK;
9797
}
9898

99+
int ARBITER_set_facts(struct ARBITER_ctx *ctx,
100+
const uint16_t *fact_ids,
101+
const int32_t *values,
102+
uint16_t count)
103+
{
104+
if (unlikely(ctx == NULL || !ctx->initialized ||
105+
fact_ids == NULL || values == NULL)) {
106+
return ARBITER_EINVAL;
107+
}
108+
109+
const arbiter_index_t fc = ctx->model->fact_count;
110+
111+
for (uint16_t i = 0; i < count; i++) {
112+
const uint16_t fid = fact_ids[i];
113+
114+
if (unlikely(fid >= fc)) {
115+
return ARBITER_ERANGE;
116+
}
117+
118+
struct ARBITER_fact_value *__restrict fv =
119+
&ctx->fact_values[fid];
120+
const int32_t old = fv->value;
121+
122+
fv->prev_value = old;
123+
fv->value = values[i];
124+
fv->valid = true;
125+
fv->changed = (values[i] != old);
126+
}
127+
128+
return ARBITER_OK;
129+
}
130+
99131
int ARBITER_snapshot_begin(struct ARBITER_ctx *ctx,
100132
struct ARBITER_snapshot *snapshot)
101133
{
@@ -109,5 +141,18 @@ int ARBITER_snapshot_begin(struct ARBITER_ctx *ctx,
109141
snapshot->timestamp_ms = k_uptime_get_32();
110142
snapshot->frozen = true;
111143

144+
#if defined(CONFIG_ARBITER_DIRTY_SKIP) && CONFIG_ARBITER_DIRTY_SKIP
145+
/* Build dirty bitmask: OR BIT(fid) for each changed fact. */
146+
uint64_t mask = 0;
147+
const arbiter_index_t fc = ctx->model->fact_count;
148+
149+
for (arbiter_index_t i = 0; i < fc && i < 64; i++) {
150+
if (ctx->fact_values[i].changed) {
151+
mask |= ((uint64_t)1 << i);
152+
}
153+
}
154+
snapshot->dirty_mask = mask;
155+
#endif
156+
112157
return ARBITER_OK;
113158
}

python/arbiter/emit_blob.py

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,7 @@
5252

5353
# Wire sizes for packed structs (all little-endian, uint16 indices)
5454
_FACT_ELEM_SIZE = 16 # id(2) + type(1) + pad(1) + range_min(4) + range_max(4) + default(4) + stale(2) + safety(1) + pad(1) => rearranged below
55-
_RULE_ELEM_SIZE = 20
55+
_RULE_ELEM_SIZE = 22
5656
_COND_ELEM_SIZE = 8
5757
_EXPR_ELEM_SIZE = 20
5858
_ACTION_ELEM_SIZE = 12
@@ -132,7 +132,7 @@ def _pack_facts(model: CanonicalModel) -> bytes:
132132
def _pack_rules(model: CanonicalModel) -> bytes:
133133
"""Pack rule definitions.
134134
135-
Wire layout per rule (20 bytes):
135+
Wire layout per rule (22 bytes):
136136
id: uint16 LE
137137
rule_class: uint8
138138
safety_critical: uint8
@@ -144,6 +144,7 @@ def _pack_rules(model: CanonicalModel) -> bytes:
144144
expr_count: uint16 LE
145145
safety_goal_id: uint16 LE
146146
set_mode: uint16 LE
147+
required_mode: uint16 LE
147148
"""
148149
buf = bytearray()
149150
cond_offset = 0
@@ -180,14 +181,18 @@ def _pack_rules(model: CanonicalModel) -> bytes:
180181
expr_start = r.get("_expr_start", 0)
181182
expr_count = r.get("_expr_count", 0)
182183

184+
# required_mode: 0xFFFF means any mode
185+
required_mode = 0xFFFF
186+
183187
buf += struct.pack(
184-
"<HBBHHHHHHHH",
188+
"<HBBHHHHHHHHH",
185189
i, rclass, safety_critical,
186190
cond_offset, cond_count,
187191
action_start, action_count,
188192
expr_start, expr_count,
189193
0xFFFF, # safety_goal_id
190194
set_mode,
195+
required_mode,
191196
)
192197
cond_offset += cond_count
193198
return bytes(buf)
@@ -348,7 +353,7 @@ def emit_blob(model: CanonicalModel) -> bytes:
348353
if model.facts:
349354
sections.append((SECTION_FACTS, facts_data, len(model.facts), fact_elem))
350355

351-
rule_elem = 20
356+
rule_elem = 22
352357
if model.rules:
353358
sections.append((SECTION_RULES, rules_data, len(model.rules), rule_elem))
354359

python/arbiter/emit_c.py

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,67 @@
4848
}
4949

5050

51+
def _compute_rule_dep_masks(model: CanonicalModel) -> list[int]:
52+
"""Compute a uint64 bitmask of fact IDs each rule depends on."""
53+
masks: list[int] = []
54+
cond_offset = 0
55+
for r in model.rules:
56+
when = r.get("when", {})
57+
cond_count = 0
58+
if isinstance(when, dict):
59+
for gk in ("all", "any", "not"):
60+
g = when.get(gk)
61+
if isinstance(g, list):
62+
cond_count += len(g)
63+
mask = 0
64+
for ci in range(cond_offset, cond_offset + cond_count):
65+
if ci < len(model.conditions):
66+
fid = model.conditions[ci].get("fact_id", 0)
67+
if fid < 64:
68+
mask |= 1 << fid
69+
masks.append(mask)
70+
cond_offset += cond_count
71+
return masks
72+
73+
74+
def _compute_required_mode(
75+
rule: dict,
76+
model: CanonicalModel,
77+
) -> str:
78+
"""Return the C literal for required_mode.
79+
80+
For mode_guard rules whose 'when' block contains an equality check
81+
on a fact named 'mode' (or similar mode fact), extract the mode value.
82+
Otherwise return UINT16_MAX (any mode).
83+
"""
84+
if rule.get("class") != "mode_guard":
85+
return "UINT16_MAX"
86+
87+
when = rule.get("when", {})
88+
if not isinstance(when, dict):
89+
return "UINT16_MAX"
90+
91+
# Scan all condition groups for mode equality checks
92+
for gk in ("all", "any"):
93+
group = when.get(gk)
94+
if not isinstance(group, list):
95+
continue
96+
for cond in group:
97+
if not isinstance(cond, dict):
98+
continue
99+
fact_name = cond.get("fact", "")
100+
op = cond.get("op", "")
101+
# Look for mode facts by checking if the fact name contains 'mode'
102+
if "mode" in fact_name.lower() and op == "==":
103+
val = cond.get("value")
104+
if isinstance(val, int):
105+
return str(val)
106+
# Value might be a mode name string
107+
if isinstance(val, str) and val in model.mode_id_map:
108+
return str(model.mode_id_map[val])
109+
return "UINT16_MAX"
110+
111+
51112
def _c_str(s: str | None) -> str:
52113
return f'"{s}"' if s else "NULL"
53114

@@ -99,6 +160,18 @@ def emit_c_header(model: CanonicalModel, emit_trace_strings: bool = True) -> str
99160

100161
lines.append("")
101162

163+
# Per-rule dependency bitmasks for dirty-flag rule skip
164+
rule_dep_masks = _compute_rule_dep_masks(model)
165+
if rule_dep_masks:
166+
lines.append("/* Per-rule input fact dependency bitmasks (dirty-flag skip). */")
167+
mask_strs = [f"UINT64_C(0x{m:016x})" for m in rule_dep_masks]
168+
lines.append(
169+
"#define ARBITER_MODEL_RULE_DEPS { "
170+
+ ", ".join(mask_strs)
171+
+ " }"
172+
)
173+
lines.append("")
174+
102175
# State defines (REQ-ARCH-039)
103176
states = getattr(model, "states", [])
104177
if states:
@@ -223,12 +296,17 @@ def emit_c_source(model: CanonicalModel, header_name: str = "arbiter_model.h",
223296

224297
expr_start = r.get("_expr_start", 0)
225298
expr_count = r.get("_expr_count", 0)
299+
300+
# Mode-aware pruning: mode_guard rules with a mode equality check
301+
required_mode = _compute_required_mode(r, model)
302+
226303
lines.append(
227304
f"\t{{ .id = {i}, .rule_class = {rclass}, "
228305
f".condition_start = {cond_offset}, .condition_count = {cond_count}, "
229306
f".action_start = {action_start}, .action_count = {action_count}, "
230307
f".expr_start = {expr_start}, .expr_count = {expr_count}, "
231308
f".safety_goal_id = UINT16_MAX, .set_mode = {set_mode}, "
309+
f".required_mode = {required_mode}, "
232310
f".safety_critical = {safety_critical}, "
233311
f".name = {name}, .explanation = {explanation} }},"
234312
)

subsys/arbiter/Kconfig

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -302,6 +302,30 @@ config ARBITER_FPGA_OFFLOAD
302302
to offload condition evaluation and expression execution
303303
to FPGA fabric. No implementation is shipped in v1.
304304

305+
# ── Dirty-Flag Rule Skip ─────────────────────────────────
306+
307+
config ARBITER_DIRTY_SKIP
308+
bool "Enable dirty-flag rule skip optimisation"
309+
default y if ARBITER_RESOLVED_STANDARD || ARBITER_RESOLVED_FULL
310+
default n
311+
help
312+
Track which facts each rule depends on. If none of a
313+
rule's input facts changed since the last evaluation,
314+
skip the rule entirely (keep previous result). Safe to
315+
disable — behaviour is identical to the unoptimised path.
316+
Requires <=64 facts (uses a uint64_t bitmask).
317+
318+
# ── Condition Result Cache ───────────────────────────────
319+
320+
config ARBITER_COND_CACHE
321+
bool "Enable condition result cache"
322+
default y
323+
help
324+
Cache the results of the 8 most recently evaluated
325+
conditions during an eval cycle. Helps when multiple
326+
rules test the same condition (e.g. temp > 80).
327+
The cache is reset at the start of each evaluation.
328+
305329
# ── Event-Driven Evaluation (REQ-ARCH-036) ───────────────
306330

307331
config ARBITER_EVENT_DRIVEN

0 commit comments

Comments
 (0)