fhueni commited on
Commit
5c8fb6b
·
1 Parent(s): bfaf968

feat: add dataset, adjust scheduler to run full dataset, implement statistic download, add model load button

Browse files
dataset/boolq_validation.csv ADDED
The diff for this file is too large to render. See raw diff
 
index.html CHANGED
@@ -25,6 +25,7 @@
25
  <h2>On-Device</h2>
26
  <label>Model (transformers.js) <input id="deviceModel" value="distilgpt2" /></label>
27
  <div id="deviceStatus">Not loaded</div>
 
28
  </div>
29
 
30
 
@@ -32,9 +33,9 @@
32
  <h2>Request Pattern</h2>
33
  <select id="patternSelect">
34
  <option value="once-per-sec">1 request / sec</option>
35
- <option value="ten-per-sec">10 requests / sec</option>
36
- <option value="batch-10-every-5s">Batch: 10 every 5s</option>
37
- <option value="burst">Burst: 50 then idle</option>
38
  </select>
39
  <label>Route strategy
40
  <select id="routeStrategy">
@@ -51,11 +52,11 @@
51
  </div>
52
  </div>
53
 
54
-
55
  <div class="card wide">
56
  <h2>Live Log & Results</h2>
57
  <div id="log" class="log"></div>
58
  <div id="stats"></div>
 
59
  </div>
60
  </section>
61
 
 
25
  <h2>On-Device</h2>
26
  <label>Model (transformers.js) <input id="deviceModel" value="distilgpt2" /></label>
27
  <div id="deviceStatus">Not loaded</div>
28
+ <button id="loadDeviceModelBtn">Load Model</button>
29
  </div>
30
 
31
 
 
33
  <h2>Request Pattern</h2>
34
  <select id="patternSelect">
35
  <option value="once-per-sec">1 request / sec</option>
36
+ <option value="every-ten-sec">Every 10 sec 1 request</option>
37
+ <option disabled value="batch-10-every-5s">(not implemented) Batch: 10 every 5s</option>
38
+ <option disabled value="burst">(not implemented) Burst: 50 then idle</option>
39
  </select>
40
  <label>Route strategy
41
  <select id="routeStrategy">
 
52
  </div>
53
  </div>
54
 
 
55
  <div class="card wide">
56
  <h2>Live Log & Results</h2>
57
  <div id="log" class="log"></div>
58
  <div id="stats"></div>
59
+ <button id="downloadStats">Download Statistics</button>
60
  </div>
61
  </section>
62
 
src/main.js CHANGED
@@ -20,22 +20,14 @@ const evaluator = new Evaluator();
20
 
21
  const requestManager = new RequestManager({
22
  deviceService: onDeviceInferenceService, cloudService: cloudInferenceService, evaluator, logger: evt => {
23
- logTo(logEl, `${evt.job.id} -> ${evt.route} | latency=${evt.latency}ms | exact=${evt.evalRes.exact} f1=${evt.evalRes.f1.toFixed(2)}`);
24
  updateStats();
25
  }
26
  });
27
 
28
 
29
  // instantiate the job scheduler with some mock prompts TODO: replace with real prompts
30
- const scheduler = new JobScheduler([
31
- {prompt: 'Translate to German: Hello world', groundTruth: 'Hallo Welt'},
32
- {
33
- prompt: 'What is 3*6?',
34
- groundTruth: '18'
35
- },
36
- {prompt: 'Answer: What is 2+2?', groundTruth: '4'},
37
- {prompt: 'What is the capital of switzerland?', groundTruth: 'Bern'}
38
- ]);
39
 
40
 
41
  scheduler.onJob(async (job) => {
@@ -87,19 +79,69 @@ document.getElementById('stopBtn').addEventListener('click', () => {
87
  document.getElementById('stopBtn').disabled = true;
88
  });
89
 
 
 
 
 
 
 
 
90
 
91
  async function loadDeviceModel() {
92
  deviceStatusEl.textContent = 'Loading...';
 
93
  try {
94
  await onDeviceInferenceService.load((s) => deviceStatusEl.textContent = s);
95
- deviceStatusEl.textContent = 'Ready';
96
  } catch (e) {
97
  deviceStatusEl.textContent = `Error: ${e.message}`;
 
98
  }
99
  }
100
 
 
 
 
 
 
 
 
 
 
 
 
 
 
101
 
 
 
 
102
  function updateStats() {
103
  const s = requestManager.stats;
104
- statsEl.innerHTML = `<pre>Processed: ${s.count}\nCloud: ${s.cloud}\nDevice: ${s.device}\nAvg latency (ms): ${s.count ? (s.totalLatencyMs / s.count).toFixed(1) : 0}\nRecent evaluations: ${Math.min(10, s.evaluations.length)}</pre>`;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
105
  }
 
20
 
21
  const requestManager = new RequestManager({
22
  deviceService: onDeviceInferenceService, cloudService: cloudInferenceService, evaluator, logger: evt => {
23
+ logTo(logEl, `${evt.job.id} -> ${evt.route} | latency=${evt.latency}ms | exact=${evt.evalRes.exact} | question="${evt.job.prompt.substring(0, 30)}..."`);
24
  updateStats();
25
  }
26
  });
27
 
28
 
29
  // instantiate the job scheduler with some mock prompts TODO: replace with real prompts
30
+ const scheduler = new JobScheduler('boolq_validation');
 
 
 
 
 
 
 
 
31
 
32
 
33
  scheduler.onJob(async (job) => {
 
79
  document.getElementById('stopBtn').disabled = true;
80
  });
81
 
82
+ document.getElementById('downloadStats').addEventListener('click', () => {
83
+ downloadStats();
84
+ });
85
+ document.getElementById('loadDeviceModelBtn').addEventListener('click', () => {
86
+ loadDeviceModel();
87
+ });
88
+
89
 
90
  async function loadDeviceModel() {
91
  deviceStatusEl.textContent = 'Loading...';
92
+ document.getElementById('loadDeviceModelBtn').disabled = true;
93
  try {
94
  await onDeviceInferenceService.load((s) => deviceStatusEl.textContent = s);
95
+ deviceStatusEl.textContent = 'Model Ready';
96
  } catch (e) {
97
  deviceStatusEl.textContent = `Error: ${e.message}`;
98
+ document.getElementById('loadDeviceModelBtn').disabled = false;
99
  }
100
  }
101
 
102
+ function downloadStats() {
103
+ const s = requestManager.stats;
104
+ // add average latency to stats for device and cloud
105
+ s.avgLatencyMs = s.count ? (s.totalLatencyMs / s.count) : 0;
106
+ s.avgDeviceLatencyMs = s.device ? (s.evaluations.filter(e => e.route === 'device').reduce((a, b) => a + b.latency, 0) / s.device) : 0;
107
+ s.avgCloudLatencyMs = s.cloud ? (s.evaluations.filter(e => e.route === 'cloud').reduce((a, b) => a + b.latency, 0) / s.cloud) : 0;
108
+
109
+ const dataStr = "data:text/json;charset=utf-8," + encodeURIComponent(JSON.stringify(s, null, 2));
110
+ const dlAnchorElem = document.createElement('a');
111
+ dlAnchorElem.setAttribute("href", dataStr);
112
+ dlAnchorElem.setAttribute("download", "stats.json");
113
+ dlAnchorElem.click();
114
+ }
115
 
116
+ /**
117
+ * Update the statistics display in the UI based on the request manager's stats
118
+ */
119
  function updateStats() {
120
  const s = requestManager.stats;
121
+
122
+ statsEl.innerHTML = `
123
+ <div style="display: flex; justify-content: space-between;">
124
+ <div>
125
+ <h3>General Stats</h3>
126
+ <pre>
127
+ Processed: ${s.count}
128
+ Avg latency (ms): ${s.count ? (s.totalLatencyMs / s.count).toFixed(1) : 0}
129
+ Recent evaluations: ${Math.min(10, s.evaluations.length)}
130
+ </pre>
131
+ </div>
132
+ <div>
133
+ <h3>Cloud Stats</h3>
134
+ <pre>
135
+ Requests: ${s.cloud}
136
+ Avg latency (ms): ${s.cloud ? (s.evaluations.filter(e => e.route === 'cloud').reduce((a, b) => a + b.latency, 0) / s.cloud).toFixed(1) : 0}
137
+ </pre>
138
+ </div>
139
+ <div>
140
+ <h3>On-Device Stats</h3>
141
+ <pre>
142
+ Requests: ${s.device}
143
+ Avg latency (ms): ${s.cloud ? (s.evaluations.filter(e => e.route === 'device').reduce((a, b) => a + b.latency, 0) / s.cloud).toFixed(1) : 0}
144
+ </pre>
145
+ </div>
146
+ </div>`;
147
  }
src/requestManager.js CHANGED
@@ -87,7 +87,7 @@ export class RequestManager {
87
  * Handle a single inference job by routing it to the appropriate service,
88
  * performing inference, evaluating the result, and recording statistics.
89
  *
90
- * @param job
91
  * @returns {Promise<{route: string, latency: number, text: string, job, evalRes: (*|XPathResult|{exact: *, f1: *})}>}
92
  */
93
  async handle(job) {
 
87
  * Handle a single inference job by routing it to the appropriate service,
88
  * performing inference, evaluating the result, and recording statistics.
89
  *
90
+ * @param job - The job object containing prompt and ground truth
91
  * @returns {Promise<{route: string, latency: number, text: string, job, evalRes: (*|XPathResult|{exact: *, f1: *})}>}
92
  */
93
  async handle(job) {
src/scheduler.js CHANGED
@@ -1,15 +1,18 @@
1
  import {sleep} from './utils.js';
2
 
3
-
4
  /**
5
  * JobScheduler emits jobs based on predefined patterns.
6
  * Can be used to simulate different load scenarios like batch processing or on-request per second
7
  */
8
  export class JobScheduler {
9
- constructor(promptSource = []) {
10
- this.promptSource = promptSource;
11
  this.running = false;
 
12
  this._onJob = null; // callback
 
 
 
13
  }
14
 
15
 
@@ -31,27 +34,27 @@ export class JobScheduler {
31
  // once per second until user stopp evaluation
32
  if (patternName === 'once-per-sec') {
33
  let i = 0;
34
- while (this.running) {
35
- this._emit(i++);
 
 
36
  await sleep(1000);
37
  }
38
- } else if (patternName === 'ten-per-sec') {
39
  let i = 0;
40
  const interval = 100; // ms
41
- while (this.running) {
42
- this._emit(i++);
 
43
  await sleep(interval);
44
  }
45
  } else if (patternName === 'batch-10-every-5s') {
46
  let i = 0;
47
  while (this.running) {
 
48
  for (let j = 0; j < 10 && this.running; j++) this._emit(i++);
49
  await sleep(5000);
50
  }
51
- } else if (patternName === 'burst') {
52
- // single burst
53
- for (let i = 0; i < 50; i++) this._emit(i);
54
- this.running = false;
55
  }
56
  }
57
 
@@ -63,17 +66,44 @@ export class JobScheduler {
63
  this.running = false;
64
  }
65
 
66
- _pickPrompt(id) {
67
- if (this.promptSource.length === 0) return {prompt: `Hello world ${id}`, groundTruth: `Hello world ${id}`};
68
- return this.promptSource[id % this.promptSource.length];
69
- }
70
-
71
 
72
- _emit(id) {
 
 
 
 
 
 
73
  if (this._onJob) {
74
- const p = this._pickPrompt(id);
75
- const job = {id: `job-${Date.now()}-${id}`, prompt: p.prompt, groundTruth: p.groundTruth};
76
  this._onJob(job);
77
  }
78
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
79
  }
 
1
  import {sleep} from './utils.js';
2
 
 
3
  /**
4
  * JobScheduler emits jobs based on predefined patterns.
5
  * Can be used to simulate different load scenarios like batch processing or on-request per second
6
  */
7
  export class JobScheduler {
8
+ constructor(datasetName = 'boolq_validation') {
9
+ // TODO implement dataset loading based on configuration parameter
10
  this.running = false;
11
+ this._dataset = null;
12
  this._onJob = null; // callback
13
+ this._datasetName = datasetName
14
+
15
+ this._loadDataset(this._datasetName);
16
  }
17
 
18
 
 
34
  // once per second until user stopp evaluation
35
  if (patternName === 'once-per-sec') {
36
  let i = 0;
37
+ while (this._dataset.length > 0 && this.running) {
38
+ console.log(this._dataset.length)
39
+ const item = this._dataset.pop();
40
+ this._emit(item);
41
  await sleep(1000);
42
  }
43
+ } else if (patternName === 'every-ten-sec') {
44
  let i = 0;
45
  const interval = 100; // ms
46
+ while (this._dataset.length > 0 && this.running) {
47
+ const item = this._dataset.pop();
48
+ this._emit(item);
49
  await sleep(interval);
50
  }
51
  } else if (patternName === 'batch-10-every-5s') {
52
  let i = 0;
53
  while (this.running) {
54
+ // TODO implement batch processing!
55
  for (let j = 0; j < 10 && this.running; j++) this._emit(i++);
56
  await sleep(5000);
57
  }
 
 
 
 
58
  }
59
  }
60
 
 
66
  this.running = false;
67
  }
68
 
 
 
 
 
 
69
 
70
+ /**
71
+ * Emit a job with the item from the dataset to process
72
+ *
73
+ * @param item - The dataset item containing prompt and ground truth
74
+ * @private
75
+ */
76
+ _emit(item) {
77
  if (this._onJob) {
78
+ const job = {prompt: item.prompt, groundTruth: item.groundTruth};
 
79
  this._onJob(job);
80
  }
81
  }
82
+
83
+ /**
84
+ * Load the dataset from CSV file based on the given name
85
+ *
86
+ * @param name - Name of the csv dataset to load without file extension
87
+ * @private
88
+ */
89
+ _loadDataset(name) {
90
+ const path = `./dataset/${name}.csv`;
91
+
92
+ fetch(path)
93
+ .then(response => {
94
+ if (!response.ok) {
95
+ throw new Error(`Dataset file not found: ${path}`);
96
+ }
97
+ return response.text();
98
+ })
99
+ .then(data => {
100
+ this._dataset = data.split('\n').slice(1).map(line => {
101
+ const [question, answer, context] = line.split(',');
102
+ return {prompt: question, groundTruth: answer};
103
+ });
104
+ })
105
+ .catch(error => {
106
+ console.error(error);
107
+ });
108
+ }
109
  }