gille1983 commited on
Commit
5eebcce
·
1 Parent(s): 1b2e36c

Add real model tensor source selection to experiment tabs

Browse files
Files changed (1) hide show
  1. app.py +176 -25
app.py CHANGED
@@ -9,6 +9,7 @@ Patent: UK Application No. 2607132.4, GB2608127.3
9
  Copyright 2026 Ryan Gillespie / Optitransfer
10
  """
11
 
 
12
  import gradio as gr
13
  import numpy as np
14
  import time
@@ -18,10 +19,154 @@ from collections import defaultdict
18
 
19
  from crdt_merge.model import CRDTMergeState
20
 
 
 
21
  ALL_STRATEGIES = sorted(CRDTMergeState.KNOWN_STRATEGIES)
22
  BASE_REQUIRED = CRDTMergeState.BASE_REQUIRED
23
  NO_BASE_STRATEGIES = sorted(set(ALL_STRATEGIES) - BASE_REQUIRED)
24
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
25
 
26
  def _make_state(strategy, base=None):
27
  """Create a CRDTMergeState, providing base if the strategy requires it."""
@@ -32,13 +177,12 @@ def _make_state(strategy, base=None):
32
 
33
  # ===== Experiment 1: Multi-Node Convergence =====
34
 
35
- def run_convergence_experiment(n_nodes, tensor_dim, strategy, n_random_orderings=5, seed=42):
36
  n_nodes, tensor_dim, n_random_orderings, seed = int(n_nodes), int(tensor_dim), int(n_random_orderings), int(seed)
37
  np.random.seed(seed)
38
  shape = (tensor_dim, tensor_dim)
39
  total_params = tensor_dim * tensor_dim * n_nodes
40
- base = np.random.randn(*shape).astype(np.float64)
41
- tensors = [np.random.randn(*shape).astype(np.float64) for _ in range(n_nodes)]
42
 
43
  log = []
44
  log.append(f"{'='*72}")
@@ -46,6 +190,7 @@ def run_convergence_experiment(n_nodes, tensor_dim, strategy, n_random_orderings
46
  log.append(f"{'='*72}")
47
  log.append(f" Nodes: {n_nodes} | Tensor: {shape} | Params: {total_params:,}")
48
  log.append(f" Strategy: {strategy} | Orderings: {n_random_orderings}")
 
49
  log.append(f"{'='*72}\n")
50
 
51
  all_resolved, all_hashes, ordering_times = [], [], []
@@ -173,18 +318,18 @@ def run_convergence_experiment(n_nodes, tensor_dim, strategy, n_random_orderings
173
 
174
  # ===== Experiment 2: Network Partition & Healing =====
175
 
176
- def run_partition_experiment(n_nodes, tensor_dim, strategy, n_partitions=3, seed=42):
177
  n_nodes, tensor_dim, n_partitions, seed = int(n_nodes), int(tensor_dim), int(n_partitions), int(seed)
178
  np.random.seed(seed)
179
  shape = (tensor_dim, tensor_dim)
180
- base = np.random.randn(*shape).astype(np.float64)
181
- tensors = [np.random.randn(*shape).astype(np.float64) for _ in range(n_nodes)]
182
 
183
  log = []
184
  log.append(f"{'='*72}")
185
  log.append(f" NETWORK PARTITION & HEALING EXPERIMENT")
186
  log.append(f"{'='*72}")
187
  log.append(f" Nodes: {n_nodes} | Partitions: {n_partitions} | Strategy: {strategy}")
 
188
  log.append(f"{'='*72}\n")
189
 
190
  nodes = []
@@ -359,12 +504,11 @@ def run_partition_experiment(n_nodes, tensor_dim, strategy, n_partitions=3, seed
359
 
360
  SLOW_STRATEGIES = {"evolutionary_merge", "genetic_merge"}
361
 
362
- def run_strategy_sweep(n_nodes, tensor_dim, seed=42, skip_slow=True, progress=gr.Progress()):
363
  n_nodes, tensor_dim, seed = int(n_nodes), int(tensor_dim), int(seed)
364
  np.random.seed(seed)
365
  shape = (tensor_dim, tensor_dim)
366
- base = np.random.randn(*shape).astype(np.float64)
367
- tensors = [np.random.randn(*shape).astype(np.float64) for _ in range(n_nodes)]
368
 
369
  strategies = ALL_STRATEGIES
370
  if skip_slow:
@@ -378,6 +522,7 @@ def run_strategy_sweep(n_nodes, tensor_dim, seed=42, skip_slow=True, progress=gr
378
  log.append(f" CROSS-STRATEGY CONVERGENCE SWEEP — ALL 26 STRATEGIES")
379
  log.append(f"{'='*72}")
380
  log.append(f" Nodes: {n_nodes} | Tensor: {shape} | Testing: {len(strategies)}/{len(ALL_STRATEGIES)}")
 
381
  if skipped:
382
  log.append(f" Skipped (slow): {', '.join(skipped)}")
383
  log.append(f"{'='*72}\n")
@@ -491,17 +636,18 @@ def run_strategy_sweep(n_nodes, tensor_dim, seed=42, skip_slow=True, progress=gr
491
 
492
  # ===== Experiment 4: Scalability Benchmark =====
493
 
494
- def run_scale_benchmark(max_nodes, tensor_dim, strategy, seed=42, progress=gr.Progress()):
495
  max_nodes, tensor_dim, seed = int(max_nodes), int(tensor_dim), int(seed)
496
  np.random.seed(seed)
497
  shape = (tensor_dim, tensor_dim)
498
- base = np.random.randn(*shape).astype(np.float64)
499
 
500
  log = []
501
  log.append(f"{'='*72}")
502
  log.append(f" SCALABILITY BENCHMARK")
503
  log.append(f"{'='*72}")
504
  log.append(f" Max nodes: {max_nodes} | Tensor: {shape} | Strategy: {strategy}")
 
505
  log.append(f"{'='*72}\n")
506
 
507
  header = f" {'Nodes':>6s} {'Params':>12s} {'Gossip':>10s} {'Resolve':>10s} {'Merges':>10s} {'Conv':>5s}"
@@ -512,7 +658,7 @@ def run_scale_benchmark(max_nodes, tensor_dim, strategy, seed=42, progress=gr.Pr
512
  if max_nodes not in steps and max_nodes >= 2:
513
  steps.append(max_nodes); steps.sort()
514
 
515
- all_tensors = [np.random.randn(*shape).astype(np.float64) for _ in range(max_nodes)]
516
  node_counts, gossip_times, resolve_times = [], [], []
517
 
518
  for si, n in enumerate(steps):
@@ -613,24 +759,24 @@ def run_scale_benchmark(max_nodes, tensor_dim, strategy, seed=42, progress=gr.Pr
613
 
614
  # ===== Full Suite =====
615
 
616
- def run_full_experiment(n_nodes, tensor_dim, strategy, n_orderings, n_partitions, seed, skip_slow, progress=gr.Progress()):
617
  all_logs, summaries = [], {}
618
 
619
  progress(0.05, "Running multi-node convergence...")
620
- l, s = run_convergence_experiment(n_nodes, tensor_dim, strategy, n_orderings, seed)
621
  all_logs.append(l); summaries["convergence"] = json.loads(s)
622
 
623
  progress(0.30, "Running partition experiment...")
624
- l, s = run_partition_experiment(n_nodes, tensor_dim, strategy, n_partitions, seed)
625
  all_logs.append(l); summaries["partition"] = json.loads(s)
626
 
627
  sweep_n = min(int(n_nodes), 10); sweep_d = min(int(tensor_dim), 64)
628
  progress(0.55, "Running strategy sweep (all 26)...")
629
- l, s = run_strategy_sweep(sweep_n, sweep_d, seed, skip_slow)
630
  all_logs.append(l); summaries["strategy_sweep"] = json.loads(s)
631
 
632
  progress(0.80, "Running scalability benchmark...")
633
- l, s = run_scale_benchmark(min(int(n_nodes), 50), sweep_d, strategy, seed)
634
  all_logs.append(l); summaries["scalability"] = json.loads(s)
635
 
636
  progress(1.0, "Complete!")
@@ -742,9 +888,10 @@ with gr.Blocks(title="CRDT-Merge Convergence Lab", theme=gr.themes.Default(prima
742
 
743
  with gr.Tabs():
744
  with gr.TabItem("Full Suite"):
745
- gr.Markdown("### Run all four experiments tests all 26 merge strategies")
746
  with gr.Row():
747
  with gr.Column(scale=1):
 
748
  n_nodes = gr.Slider(3, 100, 30, step=1, label="Nodes")
749
  tensor_dim = gr.Slider(16, 512, 128, step=16, label="Tensor Dim (d x d)")
750
  strategy = gr.Dropdown(ALL_STRATEGIES, value="weight_average", label="Primary Strategy")
@@ -756,12 +903,13 @@ with gr.Blocks(title="CRDT-Merge Convergence Lab", theme=gr.themes.Default(prima
756
  with gr.Column(scale=2):
757
  out_log = gr.Textbox(label="Experiment Log", lines=35, max_lines=80)
758
  out_json = gr.Textbox(label="JSON", lines=10, max_lines=40)
759
- run_btn.click(run_full_experiment, [n_nodes, tensor_dim, strategy, n_orderings, n_partitions, seed, skip_slow], [out_log, out_json])
760
 
761
  with gr.TabItem("Convergence"):
762
- gr.Markdown("### N nodes merge in different random orderings all must produce identical results")
763
  with gr.Row():
764
  with gr.Column(scale=1):
 
765
  c_n = gr.Slider(3, 100, 30, step=1, label="Nodes")
766
  c_d = gr.Slider(16, 512, 128, step=16, label="Tensor Dim")
767
  c_s = gr.Dropdown(ALL_STRATEGIES, value="slerp", label="Strategy")
@@ -771,12 +919,13 @@ with gr.Blocks(title="CRDT-Merge Convergence Lab", theme=gr.themes.Default(prima
771
  with gr.Column(scale=2):
772
  c_log = gr.Textbox(label="Log", lines=30, max_lines=60)
773
  c_json = gr.Textbox(label="JSON", lines=8)
774
- c_btn.click(run_convergence_experiment, [c_n, c_d, c_s, c_o, c_seed], [c_log, c_json])
775
 
776
  with gr.TabItem("Partition & Healing"):
777
  gr.Markdown("### Split nodes into isolated partitions, gossip internally, heal, verify convergence")
778
  with gr.Row():
779
  with gr.Column(scale=1):
 
780
  p_n = gr.Slider(6, 100, 30, step=1, label="Nodes")
781
  p_d = gr.Slider(16, 512, 128, step=16, label="Tensor Dim")
782
  p_s = gr.Dropdown(ALL_STRATEGIES, value="ties", label="Strategy")
@@ -786,12 +935,13 @@ with gr.Blocks(title="CRDT-Merge Convergence Lab", theme=gr.themes.Default(prima
786
  with gr.Column(scale=2):
787
  p_log = gr.Textbox(label="Log", lines=30, max_lines=60)
788
  p_json = gr.Textbox(label="JSON", lines=8)
789
- p_btn.click(run_partition_experiment, [p_n, p_d, p_s, p_p, p_seed], [p_log, p_json])
790
 
791
  with gr.TabItem("All 26 Strategies"):
792
- gr.Markdown("### Every merge strategy tested for convergence 13 base-free + 13 base-required")
793
  with gr.Row():
794
  with gr.Column(scale=1):
 
795
  sw_n = gr.Slider(3, 30, 10, step=1, label="Nodes")
796
  sw_d = gr.Slider(16, 256, 64, step=16, label="Tensor Dim")
797
  sw_seed = gr.Number(42, label="Seed", precision=0)
@@ -800,12 +950,13 @@ with gr.Blocks(title="CRDT-Merge Convergence Lab", theme=gr.themes.Default(prima
800
  with gr.Column(scale=2):
801
  sw_log = gr.Textbox(label="Log", lines=30, max_lines=60)
802
  sw_json = gr.Textbox(label="JSON", lines=8)
803
- sw_btn.click(run_strategy_sweep, [sw_n, sw_d, sw_seed, sw_skip], [sw_log, sw_json])
804
 
805
  with gr.TabItem("Scalability"):
806
  gr.Markdown("### Measure convergence overhead from 2 to N nodes")
807
  with gr.Row():
808
  with gr.Column(scale=1):
 
809
  sc_m = gr.Slider(10, 100, 50, step=5, label="Max Nodes")
810
  sc_d = gr.Slider(16, 256, 64, step=16, label="Tensor Dim")
811
  sc_s = gr.Dropdown(ALL_STRATEGIES, value="weight_average", label="Strategy")
@@ -814,7 +965,7 @@ with gr.Blocks(title="CRDT-Merge Convergence Lab", theme=gr.themes.Default(prima
814
  with gr.Column(scale=2):
815
  sc_log = gr.Textbox(label="Log", lines=30, max_lines=60)
816
  sc_json = gr.Textbox(label="JSON", lines=8)
817
- sc_btn.click(run_scale_benchmark, [sc_m, sc_d, sc_s, sc_seed], [sc_log, sc_json])
818
 
819
  gr.Markdown("---\n**crdt-merge** v0.9.5 | [GitHub](https://github.com/mgillr/crdt-merge) | [PyPI](https://pypi.org/project/crdt-merge/) | Built by Ryan Gillespie / Optitransfer | Patent: UK 2607132.4, GB2608127.3 | E4 Trust-Delta")
820
 
 
9
  Copyright 2026 Ryan Gillespie / Optitransfer
10
  """
11
 
12
+ import os
13
  import gradio as gr
14
  import numpy as np
15
  import time
 
19
 
20
  from crdt_merge.model import CRDTMergeState
21
 
22
+ HF_TOKEN = os.environ.get("HF_TOKEN", "")
23
+
24
  ALL_STRATEGIES = sorted(CRDTMergeState.KNOWN_STRATEGIES)
25
  BASE_REQUIRED = CRDTMergeState.BASE_REQUIRED
26
  NO_BASE_STRATEGIES = sorted(set(ALL_STRATEGIES) - BASE_REQUIRED)
27
 
28
+ # Architecture-compatible model families for real-weight experiments.
29
+ # Models in the same family share hidden dimensions and can be meaningfully merged.
30
+ MODEL_SOURCES = {
31
+ "Random (synthetic)": {"models": [], "hidden": 0},
32
+ "BERT Tiny (128d, 4.4M)": {
33
+ "models": ["prajjwal1/bert-tiny", "M-FAC/bert-tiny-finetuned-sst2"],
34
+ "hidden": 128,
35
+ },
36
+ "BERT Mini (256d, 11M)": {
37
+ "models": ["prajjwal1/bert-mini", "M-FAC/bert-mini-finetuned-sst2"],
38
+ "hidden": 256,
39
+ },
40
+ "BERT Small (512d, 29M)": {
41
+ "models": ["prajjwal1/bert-small", "M-FAC/bert-small-finetuned-sst2"],
42
+ "hidden": 512,
43
+ },
44
+ "BERT Base Uncased (768d, 110M)": {
45
+ "models": [
46
+ "google-bert/bert-base-uncased",
47
+ "textattack/bert-base-uncased-SST-2",
48
+ "fabriceyhc/bert-base-uncased-ag_news",
49
+ "nlptown/bert-base-multilingual-uncased-sentiment",
50
+ ],
51
+ "hidden": 768,
52
+ },
53
+ "DistilBERT Base (768d, 66M)": {
54
+ "models": [
55
+ "distilbert/distilbert-base-uncased",
56
+ "distilbert/distilbert-base-uncased-finetuned-sst-2-english",
57
+ "distilbert/distilbert-base-cased",
58
+ ],
59
+ "hidden": 768,
60
+ },
61
+ "GPT-2 Base (768d, 124M)": {
62
+ "models": [
63
+ "openai-community/gpt2",
64
+ "distilbert/distilgpt2",
65
+ ],
66
+ "hidden": 768,
67
+ },
68
+ "MiniLM-L6 (384d, 22M)": {
69
+ "models": [
70
+ "sentence-transformers/all-MiniLM-L6-v2",
71
+ "nreimers/MiniLM-L6-H384-uncased",
72
+ "sentence-transformers/paraphrase-MiniLM-L6-v2",
73
+ ],
74
+ "hidden": 384,
75
+ },
76
+ "RoBERTa Base (768d, 125M)": {
77
+ "models": [
78
+ "FacebookAI/roberta-base",
79
+ "cardiffnlp/twitter-roberta-base-sentiment-latest",
80
+ "SamLowe/roberta-base-go_emotions",
81
+ ],
82
+ "hidden": 768,
83
+ },
84
+ "MPNet Base (768d, 109M)": {
85
+ "models": [
86
+ "sentence-transformers/all-mpnet-base-v2",
87
+ "sentence-transformers/paraphrase-mpnet-base-v2",
88
+ ],
89
+ "hidden": 768,
90
+ },
91
+ "T5 Small (512d, 60M)": {
92
+ "models": [
93
+ "google-t5/t5-small",
94
+ "mrm8488/t5-small-finetuned-quora-for-paraphrasing",
95
+ ],
96
+ "hidden": 512,
97
+ },
98
+ "BLOOM 560M (1024d)": {
99
+ "models": ["bigscience/bloom-560m", "bigscience/bloomz-560m"],
100
+ "hidden": 1024,
101
+ },
102
+ "ALBERT Base (768d, 12M)": {
103
+ "models": ["albert/albert-base-v2", "textattack/albert-base-v2-SST-2"],
104
+ "hidden": 768,
105
+ },
106
+ "XLM-RoBERTa Base (768d, 278M)": {
107
+ "models": [
108
+ "FacebookAI/xlm-roberta-base",
109
+ "cardiffnlp/twitter-xlm-roberta-base-sentiment-multilingual",
110
+ ],
111
+ "hidden": 768,
112
+ },
113
+ }
114
+
115
+ MODEL_SOURCE_NAMES = list(MODEL_SOURCES.keys())
116
+
117
+ # Cache to avoid re-downloading the same model
118
+ _weight_cache = {}
119
+
120
+
121
+ def _load_model_tensor(model_id: str, target_shape: tuple):
122
+ """Load a 2D weight tensor from a HF model, sliced to target_shape."""
123
+ cache_key = (model_id, target_shape)
124
+ if cache_key in _weight_cache:
125
+ return _weight_cache[cache_key]
126
+ try:
127
+ from crdt_merge.hub.hf import HFMergeHub
128
+ hub = HFMergeHub(token=HF_TOKEN)
129
+ sd = hub.pull_weights(model_id)
130
+ for k, v in sd.items():
131
+ arr = np.array(v, dtype=np.float64) if not hasattr(v, 'astype') else v.astype(np.float64)
132
+ if arr.ndim == 2 and arr.shape[0] >= target_shape[0] and arr.shape[1] >= target_shape[1]:
133
+ result = arr[:target_shape[0], :target_shape[1]].copy()
134
+ _weight_cache[cache_key] = result
135
+ return result
136
+ except Exception:
137
+ pass
138
+ return None
139
+
140
+
141
+ def _get_tensors(n_nodes, tensor_dim, model_source, seed):
142
+ """Return (base, tensors_list, source_label) using real or synthetic weights."""
143
+ shape = (tensor_dim, tensor_dim)
144
+ rng = np.random.RandomState(seed)
145
+ family = MODEL_SOURCES.get(model_source, MODEL_SOURCES["Random (synthetic)"])
146
+ model_list = family.get("models", [])
147
+
148
+ base_tensor = None
149
+ source_label = "synthetic"
150
+
151
+ if model_list and HF_TOKEN:
152
+ base_tensor = _load_model_tensor(model_list[0], shape)
153
+ if base_tensor is not None:
154
+ source_label = f"{model_list[0]} (HF safetensors)"
155
+
156
+ if base_tensor is None:
157
+ base = rng.randn(*shape).astype(np.float64)
158
+ tensors = [rng.randn(*shape).astype(np.float64) for _ in range(n_nodes)]
159
+ else:
160
+ base = base_tensor
161
+ tensors = []
162
+ for i in range(n_nodes):
163
+ node_rng = np.random.RandomState(seed + i + 1)
164
+ t = base + node_rng.randn(*shape).astype(np.float64) * 0.05
165
+ tensors.append(t)
166
+ source_label += f" + {n_nodes} node perturbations"
167
+
168
+ return base, tensors, source_label
169
+
170
 
171
  def _make_state(strategy, base=None):
172
  """Create a CRDTMergeState, providing base if the strategy requires it."""
 
177
 
178
  # ===== Experiment 1: Multi-Node Convergence =====
179
 
180
+ def run_convergence_experiment(n_nodes, tensor_dim, strategy, n_random_orderings=5, seed=42, model_source="Random (synthetic)"):
181
  n_nodes, tensor_dim, n_random_orderings, seed = int(n_nodes), int(tensor_dim), int(n_random_orderings), int(seed)
182
  np.random.seed(seed)
183
  shape = (tensor_dim, tensor_dim)
184
  total_params = tensor_dim * tensor_dim * n_nodes
185
+ base, tensors, src_label = _get_tensors(n_nodes, tensor_dim, model_source, seed)
 
186
 
187
  log = []
188
  log.append(f"{'='*72}")
 
190
  log.append(f"{'='*72}")
191
  log.append(f" Nodes: {n_nodes} | Tensor: {shape} | Params: {total_params:,}")
192
  log.append(f" Strategy: {strategy} | Orderings: {n_random_orderings}")
193
+ log.append(f" Tensor source: {src_label}")
194
  log.append(f"{'='*72}\n")
195
 
196
  all_resolved, all_hashes, ordering_times = [], [], []
 
318
 
319
  # ===== Experiment 2: Network Partition & Healing =====
320
 
321
+ def run_partition_experiment(n_nodes, tensor_dim, strategy, n_partitions=3, seed=42, model_source="Random (synthetic)"):
322
  n_nodes, tensor_dim, n_partitions, seed = int(n_nodes), int(tensor_dim), int(n_partitions), int(seed)
323
  np.random.seed(seed)
324
  shape = (tensor_dim, tensor_dim)
325
+ base, tensors, src_label = _get_tensors(n_nodes, tensor_dim, model_source, seed)
 
326
 
327
  log = []
328
  log.append(f"{'='*72}")
329
  log.append(f" NETWORK PARTITION & HEALING EXPERIMENT")
330
  log.append(f"{'='*72}")
331
  log.append(f" Nodes: {n_nodes} | Partitions: {n_partitions} | Strategy: {strategy}")
332
+ log.append(f" Tensor source: {src_label}")
333
  log.append(f"{'='*72}\n")
334
 
335
  nodes = []
 
504
 
505
  SLOW_STRATEGIES = {"evolutionary_merge", "genetic_merge"}
506
 
507
+ def run_strategy_sweep(n_nodes, tensor_dim, seed=42, skip_slow=True, model_source="Random (synthetic)", progress=gr.Progress()):
508
  n_nodes, tensor_dim, seed = int(n_nodes), int(tensor_dim), int(seed)
509
  np.random.seed(seed)
510
  shape = (tensor_dim, tensor_dim)
511
+ base, tensors, src_label = _get_tensors(n_nodes, tensor_dim, model_source, seed)
 
512
 
513
  strategies = ALL_STRATEGIES
514
  if skip_slow:
 
522
  log.append(f" CROSS-STRATEGY CONVERGENCE SWEEP — ALL 26 STRATEGIES")
523
  log.append(f"{'='*72}")
524
  log.append(f" Nodes: {n_nodes} | Tensor: {shape} | Testing: {len(strategies)}/{len(ALL_STRATEGIES)}")
525
+ log.append(f" Tensor source: {src_label}")
526
  if skipped:
527
  log.append(f" Skipped (slow): {', '.join(skipped)}")
528
  log.append(f"{'='*72}\n")
 
636
 
637
  # ===== Experiment 4: Scalability Benchmark =====
638
 
639
+ def run_scale_benchmark(max_nodes, tensor_dim, strategy, seed=42, model_source="Random (synthetic)", progress=gr.Progress()):
640
  max_nodes, tensor_dim, seed = int(max_nodes), int(tensor_dim), int(seed)
641
  np.random.seed(seed)
642
  shape = (tensor_dim, tensor_dim)
643
+ base, all_tensors_init, src_label = _get_tensors(max_nodes, tensor_dim, model_source, seed)
644
 
645
  log = []
646
  log.append(f"{'='*72}")
647
  log.append(f" SCALABILITY BENCHMARK")
648
  log.append(f"{'='*72}")
649
  log.append(f" Max nodes: {max_nodes} | Tensor: {shape} | Strategy: {strategy}")
650
+ log.append(f" Tensor source: {src_label}")
651
  log.append(f"{'='*72}\n")
652
 
653
  header = f" {'Nodes':>6s} {'Params':>12s} {'Gossip':>10s} {'Resolve':>10s} {'Merges':>10s} {'Conv':>5s}"
 
658
  if max_nodes not in steps and max_nodes >= 2:
659
  steps.append(max_nodes); steps.sort()
660
 
661
+ all_tensors = all_tensors_init if len(all_tensors_init) >= max_nodes else [np.random.randn(*shape).astype(np.float64) for _ in range(max_nodes)]
662
  node_counts, gossip_times, resolve_times = [], [], []
663
 
664
  for si, n in enumerate(steps):
 
759
 
760
  # ===== Full Suite =====
761
 
762
+ def run_full_experiment(n_nodes, tensor_dim, strategy, n_orderings, n_partitions, seed, skip_slow, model_source="Random (synthetic)", progress=gr.Progress()):
763
  all_logs, summaries = [], {}
764
 
765
  progress(0.05, "Running multi-node convergence...")
766
+ l, s = run_convergence_experiment(n_nodes, tensor_dim, strategy, n_orderings, seed, model_source)
767
  all_logs.append(l); summaries["convergence"] = json.loads(s)
768
 
769
  progress(0.30, "Running partition experiment...")
770
+ l, s = run_partition_experiment(n_nodes, tensor_dim, strategy, n_partitions, seed, model_source)
771
  all_logs.append(l); summaries["partition"] = json.loads(s)
772
 
773
  sweep_n = min(int(n_nodes), 10); sweep_d = min(int(tensor_dim), 64)
774
  progress(0.55, "Running strategy sweep (all 26)...")
775
+ l, s = run_strategy_sweep(sweep_n, sweep_d, seed, skip_slow, model_source)
776
  all_logs.append(l); summaries["strategy_sweep"] = json.loads(s)
777
 
778
  progress(0.80, "Running scalability benchmark...")
779
+ l, s = run_scale_benchmark(min(int(n_nodes), 50), sweep_d, strategy, seed, model_source)
780
  all_logs.append(l); summaries["scalability"] = json.loads(s)
781
 
782
  progress(1.0, "Complete!")
 
888
 
889
  with gr.Tabs():
890
  with gr.TabItem("Full Suite"):
891
+ gr.Markdown("### Run all four experiments -- tests all 26 merge strategies")
892
  with gr.Row():
893
  with gr.Column(scale=1):
894
+ model_src = gr.Dropdown(MODEL_SOURCE_NAMES, value="Random (synthetic)", label="Tensor Source (safetensors)")
895
  n_nodes = gr.Slider(3, 100, 30, step=1, label="Nodes")
896
  tensor_dim = gr.Slider(16, 512, 128, step=16, label="Tensor Dim (d x d)")
897
  strategy = gr.Dropdown(ALL_STRATEGIES, value="weight_average", label="Primary Strategy")
 
903
  with gr.Column(scale=2):
904
  out_log = gr.Textbox(label="Experiment Log", lines=35, max_lines=80)
905
  out_json = gr.Textbox(label="JSON", lines=10, max_lines=40)
906
+ run_btn.click(run_full_experiment, [n_nodes, tensor_dim, strategy, n_orderings, n_partitions, seed, skip_slow, model_src], [out_log, out_json])
907
 
908
  with gr.TabItem("Convergence"):
909
+ gr.Markdown("### N nodes merge in different random orderings -- all must produce identical results")
910
  with gr.Row():
911
  with gr.Column(scale=1):
912
+ c_src = gr.Dropdown(MODEL_SOURCE_NAMES, value="Random (synthetic)", label="Tensor Source (safetensors)")
913
  c_n = gr.Slider(3, 100, 30, step=1, label="Nodes")
914
  c_d = gr.Slider(16, 512, 128, step=16, label="Tensor Dim")
915
  c_s = gr.Dropdown(ALL_STRATEGIES, value="slerp", label="Strategy")
 
919
  with gr.Column(scale=2):
920
  c_log = gr.Textbox(label="Log", lines=30, max_lines=60)
921
  c_json = gr.Textbox(label="JSON", lines=8)
922
+ c_btn.click(run_convergence_experiment, [c_n, c_d, c_s, c_o, c_seed, c_src], [c_log, c_json])
923
 
924
  with gr.TabItem("Partition & Healing"):
925
  gr.Markdown("### Split nodes into isolated partitions, gossip internally, heal, verify convergence")
926
  with gr.Row():
927
  with gr.Column(scale=1):
928
+ p_src = gr.Dropdown(MODEL_SOURCE_NAMES, value="Random (synthetic)", label="Tensor Source (safetensors)")
929
  p_n = gr.Slider(6, 100, 30, step=1, label="Nodes")
930
  p_d = gr.Slider(16, 512, 128, step=16, label="Tensor Dim")
931
  p_s = gr.Dropdown(ALL_STRATEGIES, value="ties", label="Strategy")
 
935
  with gr.Column(scale=2):
936
  p_log = gr.Textbox(label="Log", lines=30, max_lines=60)
937
  p_json = gr.Textbox(label="JSON", lines=8)
938
+ p_btn.click(run_partition_experiment, [p_n, p_d, p_s, p_p, p_seed, p_src], [p_log, p_json])
939
 
940
  with gr.TabItem("All 26 Strategies"):
941
+ gr.Markdown("### Every merge strategy tested for convergence -- 13 base-free + 13 base-required")
942
  with gr.Row():
943
  with gr.Column(scale=1):
944
+ sw_src = gr.Dropdown(MODEL_SOURCE_NAMES, value="Random (synthetic)", label="Tensor Source (safetensors)")
945
  sw_n = gr.Slider(3, 30, 10, step=1, label="Nodes")
946
  sw_d = gr.Slider(16, 256, 64, step=16, label="Tensor Dim")
947
  sw_seed = gr.Number(42, label="Seed", precision=0)
 
950
  with gr.Column(scale=2):
951
  sw_log = gr.Textbox(label="Log", lines=30, max_lines=60)
952
  sw_json = gr.Textbox(label="JSON", lines=8)
953
+ sw_btn.click(run_strategy_sweep, [sw_n, sw_d, sw_seed, sw_skip, sw_src], [sw_log, sw_json])
954
 
955
  with gr.TabItem("Scalability"):
956
  gr.Markdown("### Measure convergence overhead from 2 to N nodes")
957
  with gr.Row():
958
  with gr.Column(scale=1):
959
+ sc_src = gr.Dropdown(MODEL_SOURCE_NAMES, value="Random (synthetic)", label="Tensor Source (safetensors)")
960
  sc_m = gr.Slider(10, 100, 50, step=5, label="Max Nodes")
961
  sc_d = gr.Slider(16, 256, 64, step=16, label="Tensor Dim")
962
  sc_s = gr.Dropdown(ALL_STRATEGIES, value="weight_average", label="Strategy")
 
965
  with gr.Column(scale=2):
966
  sc_log = gr.Textbox(label="Log", lines=30, max_lines=60)
967
  sc_json = gr.Textbox(label="JSON", lines=8)
968
+ sc_btn.click(run_scale_benchmark, [sc_m, sc_d, sc_s, sc_seed, sc_src], [sc_log, sc_json])
969
 
970
  gr.Markdown("---\n**crdt-merge** v0.9.5 | [GitHub](https://github.com/mgillr/crdt-merge) | [PyPI](https://pypi.org/project/crdt-merge/) | Built by Ryan Gillespie / Optitransfer | Patent: UK 2607132.4, GB2608127.3 | E4 Trust-Delta")
971