Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
622f0bc
Add EigenTrust reputation algorithm: implementation, tests, benchmark
claude Apr 23, 2026
e85e6d0
Add Posit32 and PVec EigenTrust variants; 4-way performance comparison
claude Apr 23, 2026
f3097bd
Add tools/bench-phases.rkt; reveal real algorithm cost via PHASE-TIMINGS
claude Apr 23, 2026
b5dc26f
Add multi-step W3 workload (c-asym-3, alpha=3/10, 3 forced iters); fi…
claude Apr 23, 2026
d873c9e
Switch to column-stochastic convention; enforce invariant; add ring f…
claude Apr 23, 2026
f66943e
Populate phase-breakdown table for ring-4 workload (column-stochastic…
claude Apr 23, 2026
04219ac
Skip stale bench-bsp-le-track2.rkt during raco setup
claude Apr 24, 2026
06d52c0
EigenTrust on propagators: 5th variant, ~115_000x faster than List+Rat
claude Apr 29, 2026
842d7b2
EigenTrust on propagators: add float + fine-grained variants; 7-way c…
claude Apr 29, 2026
eaa7e12
Add scaling benchmark across n for the 3 propagator variants
claude Apr 30, 2026
407efb7
Scaling benchmark: split rat (n<=128) from float (n>128); document n=…
claude Apr 30, 2026
235453b
Scaling bench: per-sample 30 s timeout; mark intractable cells TIMEOUT
claude Apr 30, 2026
ab64982
Scaling-vs-n results: float scales to n=4096 in 1.46s; rat times out …
claude Apr 30, 2026
206d3ec
Add plain (no-propagator) Racket eigentrust + extend scaling bench
claude May 1, 2026
475b940
9-way comparison: add plain Racket numbers, isolate propagator overhead
claude May 1, 2026
b8df213
Reframe comparison around 4 axes: math vs network vs Prologos math (+…
claude May 1, 2026
6a23eca
W3-only Prologos fixture: isolate reduce_ms to a single eigentrust call
claude May 1, 2026
f15701d
Refresh prologos.data.nat .pnet cache after bench runs
claude May 1, 2026
788b62f
Extended scaling bench n=4096..16384; trend fit; breakeven analysis
claude May 1, 2026
4e6a10c
Parallel BSP experiment: fine-grained float variant + executor sweep
claude May 1, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
400 changes: 400 additions & 0 deletions docs/tracking/2026-04-23_eigentrust_comparison.md

Large diffs are not rendered by default.

410 changes: 410 additions & 0 deletions docs/tracking/2026-04-23_eigentrust_pitfalls.md

Large diffs are not rendered by default.

129 changes: 129 additions & 0 deletions docs/tracking/2026-04-29_0650_eigentrust-on-propagators.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
# EigenTrust on Propagators (Racket-direct, 5th comparison variant)

**Track**: PR #2 follow-up — direct Racket-on-propagators eigentrust to
compare against the four Prologos surface variants.

## Summary

The four existing variants (List + Rat, List + Posit32, PVec + Rat,
PVec + Posit32 — see `2026-04-23_eigentrust_comparison.md`) are all
Prologos surface programs whose `reduce_ms` we measure through
`process-file`. They share the Prologos elaboration / type-check
overhead and the exact-Rat denominator dynamics.

This track adds a 5th variant: a direct Racket implementation of
EigenTrust that uses Prologos's underlying propagator network
infrastructure (`make-prop-network`, `net-new-cell`,
`net-add-propagator`, `run-to-quiescence-bsp`) but bypasses the
Prologos surface language entirely. The measurement isolates "how
fast is the propagator infrastructure on this workload" from "how
fast is Prologos elaboration + reduction on this workload".

## Design

### Architecture: chain-of-cells, one per iteration

```
+---+ step-1 +---+ step-2 +---+ step-3 +---+
|t_0| ---------> |t_1| ---------> |t_2| ---------> |t_3|
+---+ +---+ +---+ +---+
```

- **Cells**: `t_0`, `t_1`, …, `t_K` — each holds the entire trust
vector at iteration k, as a Racket value. The `t_0` cell is
pre-loaded with the pre-trust vector p.
- **Constants on-network**: `m-cid` (matrix), `p-cid` (pre-trust),
`alpha-cid` (damping). Each is a fire-once-write cell with
last-write-wins merge.
- **Propagators**: K fire-once propagators. `step-k` reads
(`alpha`, `m`, `p`, `t_{k-1}`) and writes `t_k = (1-α)·M·t + α·p`.
Fire-once because each step happens exactly once (no re-fire).
- **Final read**: after BSP quiescence, `net-cell-read net t_K-cid`
returns the result vector.

### Cell merge

Trust vectors are not naturally monotone (each step replaces).
Merge is **last-write-wins**: `(λ (old new) new)`. Combined with
fire-once, this gives "exactly one write" semantics — the cell value
is the propagator's output. (For a true monotone formulation we
would need iteration-indexed lattice values, but that adds
complexity without illuminating the comparison; documented as a
follow-up.)

### Workload

Match the Prologos benchmarks' W3 (the dominant workload):
**4-peer ring matrix, pre-trust concentrated on peer 0, α = 3/10,
3 forced iterations** (eps = 0/1, budget = 3). The expected
result after 3 iterations is

```
[5401/10000, 21/100, 147/1000, 1029/10000]
```

— same hand-computed trajectory as the Prologos versions, derivable
from `t_{k+1} = 7/10·M·t_k + 3/10·p`.

## Network Reality Check

Per `.claude/rules/workflow.md` § "Network Reality Check":

1. **`net-add-propagator` calls?** K calls (one per step) → installation phase.
2. **`net-cell-write` calls produce the result?** K writes (one per `t_k`); final answer is read from `t_K`'s cell.
3. **cell creation → prop installation → cell write → cell read = result?** Yes — direct chain.

The matrix-vector multiply inside the fire function is off-network Racket compute. This is acknowledged debt: the on-network cell flow carries trust vectors between iterations; the per-step computation is opaque to the network. A finer-grained variant (one cell per peer per iteration, K·n propagators each doing one row's dot product) would be more on-network but at higher constant cost — out of scope for the comparison-vs-Prologos goal.

## Progress Tracker

| Phase | Description | Status | Notes |
|---|---|---|---|
| 0 | Tracking doc (this file) | ✅ | – |
| 1 | Implementation: chain-of-cells eigentrust on propagators | ✅ | `benchmarks/comparative/eigentrust-propagators.rkt` — plain propagators (not fire-once); ring-4 W3 returns matching `[5401/10000, 21/100, 147/1000, 1029/10000]` |
| 2 | rackunit test: verify ring-4 result matches the Prologos variants | ✅ | `tests/test-eigentrust-propagators.rkt` — 13 tests (col-stochastic check, off-network kernel, end-to-end, mass preservation), all pass |
| 3 | Benchmark runner integrated with `tools/bench-micro.rkt`-style timing | ✅ | `benchmarks/comparative/eigentrust-propagators-bench.rkt` — W1/W2/W3/W3-deep, 5-run median |
| 4 | 5-way comparison numbers; update `2026-04-23_eigentrust_comparison.md` | ✅ | W3 reduce_ms = 0.16 ms vs Prologos List+Rat 18 372 ms (~115 000× faster); k=10 tractable where surface variants O(k²) blow up |

## Headline result

| Variant | reduce_ms (W3, ring k=4) |
|--------------------|--------------------------|
| List + Rat (P) | 18 372 |
| List + Posit32 (P) | 17 990 |
| PVec + Rat (P) | 33 006 |
| PVec + Posit32 (P) | 33 198 |
| **propagators** | **0.16** |

The propagator-direct version is ~115 000× faster because it
bypasses Prologos elaboration + reduction — both versions perform
identical Racket-level rational arithmetic. The gap measures the
cost of the Prologos surface language + reducer on this workload.

## Network Reality Check (final)

1. **`net-add-propagator` calls?** K (= 4 for W3) plain propagators installed.
2. **`net-cell-write` calls produce the result?** K writes to t_1..t_K cells; the final answer is read from t_K.
3. **cell creation → prop installation → cell write → cell read = result?** Yes. After construction, BSP runs; after K rounds the chain has settled; final cell is read.

The matrix-vector multiply inside each fire function is off-network
Racket compute (Racket's vector + rational ops). The on-network
flow is the trust-vector cell sequence between iterations. A
fine-grained variant (one cell per peer per iteration, K·n
propagators each doing one row's dot product) is documented in
the design as out-of-scope; the K-step chain is the simplest
on-network expression of the iteration that satisfies the Reality
Check.

## Files

- `racket/prologos/benchmarks/comparative/eigentrust-propagators.rkt` — implementation + main entry
- `racket/prologos/tests/test-eigentrust-propagators.rkt` — rackunit
- `docs/tracking/2026-04-23_eigentrust_comparison.md` — updated with 5th-variant numbers (Phase 4)

## Out of scope

- Fine-grained per-peer cells (K·n propagators)
- Convergence-driven version (eps-based) — only forced-iteration here, matches W3
- Iteration-indexed lattice values for true monotone semantics
- Rewriting the Prologos surface variants to use the same fixture set (already done in PR #2)
145 changes: 145 additions & 0 deletions docs/tracking/2026-04-29_eigentrust_propagators_pitfalls.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
# EigenTrust on Propagators — Pitfalls

_Track-specific landmines surfaced while implementing EigenTrust
directly on Prologos's propagator network infrastructure
(2026-04-29). For the surface-language pitfalls see
`2026-04-23_eigentrust_pitfalls.md`._

## 1. Fire-once + initial scheduling breaks chained propagators

**Expected:**
```racket
;; Chain: t_0 → step-1 → t_1 → step-2 → t_2 → ... → t_K.
;; Use net-add-fire-once-propagator because each step fires exactly once.
(net-add-fire-once-propagator net inputs outputs fire-fn)
```

**Actual:** all K fire-once propagators get scheduled at install
time (since their inputs already have values — t_0 was pre-loaded
with p, the constants are pre-loaded), then BSP fires them ALL
in round 1 on the same initial snapshot. Only step-1's read of
its input (t_0) sees a useful value (p); step-2..step-K read their
input cells which are still at their initial value (zeros).
Each step writes `α·p` (the formula's other term) and that's the
final result — every iteration cell ends up holding `α·p`, not the
correct trajectory.

**Why fire-once doesn't compose with chains:** fire-once means
"fire AT MOST ONCE per propagator". Once a propagator fires in
round 1, it never fires again — even if its input cell changes
later via a chain-upstream write. A chain depends on
re-firing-on-input-change, which is what _plain_ propagators do.

**Fix:** use `net-add-propagator` (no fire-once flag). Plain
propagators re-fire on input changes. After the first BSP round
all step-k cells have wrong values, but only t_1 (= step-1's output
from a correct read of t_0) is useful. In round 2, step-2 sees
t_1's correct value and re-fires (its other inputs — M, p, α —
haven't changed but cell-change detection only needs ONE input to
have changed). After K BSP rounds the chain has fully settled.

**Suggested doc fix:** the propagator-design checklist in
`.claude/rules/propagator-design.md` already says
> "If the propagator writes a result and never needs to fire again
> (narrower, contradiction detector, type-write, usage-write,
> constraint-creation), use `net-add-fire-once-propagator`."

Add a counter-example: "If the propagator is part of a chain
where it must fire when an upstream sibling produces output, use
plain `net-add-propagator`. Fire-once + chain = wrong result on
the first round." Or, equivalently: "fire-once is for
single-direction writes from inputs that are SET BEFORE the
propagator is installed — not for chains where some upstream input
is written by another propagator after install."

**Diagnostic clue:** if a chained propagator network produces the
"residual term only" (in EigenTrust: `α·p` instead of
`(1-α)·M·t_K + α·p`), it's the fire-once-on-uninitialised-input
pattern.

## 2. Cell merge function semantics during initial-value reads vs writes

**Observation:** `net-new-cell net initial-value merge-fn` stores
`initial-value` directly. The merge function is invoked only on
subsequent `net-cell-write` calls (`new = merge-fn(old, new-from-write)`).
A propagator that reads a cell BEFORE any write sees the initial
value untransformed — `merge-fn` plays no role.

**Why this matters:** in the chain implementation, the trust-vector
cells `t_1, …, t_K` are initialised to a zero vector (placeholder)
and overwritten by their step propagator. The first write goes
through `merge-fn`, but with `initial = zeros` and `new = step-result`
the merge result must be `step-result` regardless of `initial`.
We use `(λ (old new) new)` (last-write-wins). If we'd used a
"pointwise max" or "set-union" merge on a value-typed cell, the
zero-vector initial would survive the first merge in some
positions and silently corrupt the iterate.

**Suggested practice:** for value-typed (non-monotone) cells, use
last-write-wins explicitly. Combined with plain (not fire-once)
propagators, this gives "the cell holds the latest computed value"
semantics — which is what most numerical iterations want.

## 3. Vector equality of inexact (float) values fails `equal?`

**Observation:** Racket's `equal?` on two `(vector 0.1 0.2 0.3)`
values is `#t` only if the byte-identical floats are stored. After
arithmetic (e.g. `(+ 0.1 0.2)` vs `(+ 0.2 0.1)` — which can produce
different bit patterns due to non-commutative round-off) `equal?`
returns `#f` even though the values are mathematically equal.

**Consequence for tests:** rackunit's `check-equal?` against an
expected float vector is brittle. Use `check-=` with an explicit
tolerance, or compare each component within an epsilon, or check
properties (mass-preservation, magnitude bounds) rather than exact
equality.

**Workaround in this work:** the float variant's tests check that
the result is within ε of the exact-rational result. We compute
the rational version (which has `equal?`-stable output) and then
verify `|float-result[i] - rational-result[i]| < eps` for each i.

## 4. `for/sum` over an empty vector returns 0 (an exact integer), not 0.0

**Observation:** `(for/sum ([x (in-vector (vector))]) x)` returns
`0`, the exact integer. When the rest of the algorithm expects
flonums, this propagates an exact 0 through subsequent arithmetic.
Mixed exact/inexact arithmetic in Racket promotes to inexact, so
correctness is preserved, but type predicates like `flonum?` or
`(real? x)` checks may give surprising answers.

**Workaround:** seed `for/sum` with `0.0` explicitly:
```racket
(for/sum ([x (in-vector v)] #:when #t) x) ;; 0 for empty
;; vs
(for/fold ([acc 0.0]) ([x (in-vector v)]) (+ acc x)) ;; 0.0 for empty
```

This is a Racket library hiccup (not a Prologos one), but it bites
when constructing the initial accumulator for a flonum dot product
on an empty vector — though for non-empty matrices we never hit
the empty case.

## 5. Fine-grained per-peer variant: cell-id arrays vs lookup overhead

**Observation:** the fine-grained variant allocates `K·n` cells
(one per peer per iteration) plus 4 constant cells. For W3
(K=4, n=4) that's 20 cells. Each fire function reads n+3 inputs
(`t_{k-1, 0..n-1}`, `M[i,:]` constant, `p[i]`, `alpha`).

**Naive implementation pitfall:** if you store cell-ids in a list
and `list-ref` in the fire function, you pay O(n) lookup per access
times O(n) access count = O(n²) per fire. For small n this is
swamped by `net-cell-read` costs but at scale it matters.

**Workaround:** store cell-ids in a vector of vectors
(`(vector-ref (vector-ref t-cids k) i)`) for O(1) access. Closures
capture the vector once at install time.

**Bonus:** the per-peer variant is more "on-network" (each peer's
trust is its own cell) but also has higher constant overhead. For
small n (≤ 4) the coarse variant is faster; for larger n the
fine-grained version's parallel-fire potential MIGHT win, but at
the matrix sizes EigenTrust is typically benchmarked on (n ≤ 100),
the constant-factor overhead per cell ($cell-id allocation + CHAMP
insert + dependent registration) dominates.
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
ns benchmarks::comparative::eigentrust-list-posit-w3only

;; ============================================================
;; Benchmark: EigenTrust — List + Posit32 variant
;; See eigentrust-list-rat.prologos for the workload specification.
;; ============================================================

spec dot [List Posit32] [List Posit32] -> Posit32
defn dot [xs ys]
match xs
| nil -> ~0.0
| cons x as -> match ys
| nil -> ~0.0
| cons y bs -> p32+ [p32* x y] [dot as bs]

spec scale-vec Posit32 [List Posit32] -> [List Posit32]
defn scale-vec [s xs]
match xs
| nil -> nil
| cons x as -> cons [p32* s x] [scale-vec s as]

spec add-vec [List Posit32] [List Posit32] -> [List Posit32]
defn add-vec [xs ys]
match xs
| nil -> nil
| cons x as -> match ys
| nil -> nil
| cons y bs -> cons [p32+ x y] [add-vec as bs]

spec sub-vec [List Posit32] [List Posit32] -> [List Posit32]
defn sub-vec [xs ys]
match xs
| nil -> nil
| cons x as -> match ys
| nil -> nil
| cons y bs -> cons [p32- x y] [sub-vec as bs]

spec p32-max Posit32 Posit32 -> Posit32
defn p32-max [a b]
match [p32-lt a b]
| true -> b
| false -> a

spec linf-norm [List Posit32] -> Posit32
defn linf-norm [xs]
match xs
| nil -> ~0.0
| cons x as -> p32-max [p32-abs x] [linf-norm as]

spec mat-vec-mul [List [List Posit32]] [List Posit32] -> [List Posit32]
defn mat-vec-mul [m t]
match m
| nil -> nil
| cons r rs -> cons [dot r t] [mat-vec-mul rs t]

spec col-sums [List [List Posit32]] -> [List Posit32]
defn col-sums [m]
match m
| nil -> nil
| cons r rest -> match rest
| nil -> r
| cons _ _ -> add-vec r [col-sums rest]

spec all-ones? [List Posit32] -> Bool
defn all-ones? [xs]
match xs
| nil -> true
| cons x as -> match [p32-eq x ~1.0]
| false -> false
| true -> all-ones? as

spec col-stochastic? [List [List Posit32]] -> Bool
defn col-stochastic? [m]
all-ones? [col-sums m]

spec eigentrust-step [List [List Posit32]] [List Posit32] Posit32 [List Posit32] -> [List Posit32]
defn eigentrust-step [m p alpha t]
add-vec [scale-vec [p32- ~1.0 alpha] [mat-vec-mul m t]] [scale-vec alpha p]

spec eigentrust-iterate [List [List Posit32]] [List Posit32] Posit32 Posit32 Int [List Posit32] [List Posit32] -> [List Posit32]
defn eigentrust-iterate [m p alpha eps budget t tnew]
match [int-le budget 0]
| true -> tnew
| false -> match [p32-lt [linf-norm [sub-vec tnew t]] eps]
| true -> tnew
| false -> eigentrust-iterate m p alpha eps [int- budget 1] tnew [eigentrust-step m p alpha tnew]

spec eigentrust [List [List Posit32]] [List Posit32] Posit32 Posit32 Int -> [List Posit32]
defn eigentrust [m p alpha eps max-iter]
match [col-stochastic? m]
| false -> (the [List Posit32] [panic "eigentrust: M must be column-stochastic"])
| true -> eigentrust-iterate m p alpha eps max-iter p [eigentrust-step m p alpha p]


;; ============================================================
;; Fixtures
;; ============================================================

def p-uniform-4 : [List Posit32] := '[~0.25 ~0.25 ~0.25 ~0.25]
def p-uniform-3 : [List Posit32] := '[~0.33333 ~0.33333 ~0.33333]
def p-seed-0 : [List Posit32] := '[~1.0 ~0.0 ~0.0 ~0.0]

def m-uniform-4 : [List [List Posit32]] := '['[~0.25 ~0.25 ~0.25 ~0.25] '[~0.25 ~0.25 ~0.25 ~0.25] '[~0.25 ~0.25 ~0.25 ~0.25] '[~0.25 ~0.25 ~0.25 ~0.25]]

def m-others-3 : [List [List Posit32]] := '['[~0.0 ~0.5 ~0.5] '[~0.5 ~0.0 ~0.5] '[~0.5 ~0.5 ~0.0]]

def m-ring-4 : [List [List Posit32]] := '['[~0.0 ~0.0 ~0.0 ~1.0] '[~1.0 ~0.0 ~0.0 ~0.0] '[~0.0 ~1.0 ~0.0 ~0.0] '[~0.0 ~0.0 ~1.0 ~0.0]]


;; ============================================================
;; W3 ONLY — for isolating reduce_ms attributable to the iteration.
;; Same workload as the W3 line in eigentrust-list-posit.prologos.
;; max-iter=3 ⇒ k=4 step calls (entry pre-computes step 1).
;; ============================================================

eigentrust m-ring-4 p-seed-0 ~0.3 ~0.0 3
Loading
Loading