RabbitDaisuke commited on
Commit
08071f9
·
verified ·
1 Parent(s): b7c1b2a

Update browser execution-path benchmark f582bf55550ed0b5efbb21e8606f75d80918ca31

Browse files
benchmark/benchmark.js CHANGED
@@ -1,8 +1,9 @@
 
1
  const $=id=>document.getElementById(id),REPO='RabbitDaisuke/rtmw3d-webgpu-onnx';
2
  const video=$('video'),crop=$('crop'),ctx=crop.getContext('2d',{willReadFrequently:true});
3
- let catalog,revision,base,ort,stopped=false,results=[],videoURL;
 
4
  const status=s=>$('status').textContent=s;
5
- const pct=(values,p)=>{const s=values.toSorted((a,b)=>a-b);return s.length?s[Math.floor((s.length-1)*p)]:null;};
6
  async function hash(bytes){return [...new Uint8Array(await crypto.subtle.digest('SHA-256',bytes))].map(x=>x.toString(16).padStart(2,'0')).join('');}
7
  async function get(url){const r=await fetch(url,{credentials:'omit',referrerPolicy:'no-referrer'});if(!r.ok)throw Error(`${r.status}: ${url}`);return r;}
8
  async function bytes(file,sha){
@@ -11,16 +12,17 @@ async function bytes(file,sha){
11
  status(`取得・照合中: ${file}`);r=await get(key);const b=await r.arrayBuffer();if(await hash(b)!==sha)throw Error(`SHA-256不一致: ${file}`);
12
  try{await cache.put(key,new Response(b));}catch{status('キャッシュ容量不足。取得済みモデルで計測を続けます。');}return b;
13
  }
14
- async function loadRuntime(){
15
- if(ort)return ort;if(!navigator.gpu)throw Error('WebGPU対応のPCブラウザで開いてください');
16
  const adapter=await navigator.gpu.requestAdapter({powerPreference:'high-performance'});if(!adapter)throw Error('WebGPUアダプターを取得できません');
17
- const manifest=catalog.runtime,entry=manifest.files.find(f=>f.path===manifest.entrypoint),glue=manifest.files.find(f=>f.path.endsWith('.jsep.mjs'));
 
18
  const js=URL.createObjectURL(new Blob([await bytes(entry.path,entry.sha256)],{type:'text/javascript'}));
19
  const module=URL.createObjectURL(new Blob([await bytes(glue.path,glue.sha256)],{type:'text/javascript'}));
20
  ort=await import(js);ort.env.wasm.numThreads=1;ort.env.wasm.proxy=false;
21
  ort.env.wasm.wasmPaths={mjs:module,wasm:base+'runtime/ort-wasm-simd-threaded.jsep.wasm'};
22
- ort.env.webgpu.powerPreference='high-performance';
23
- return ort;
24
  }
25
  async function seek(t){if(Math.abs(video.currentTime-t)<.001&&video.readyState>=2)return;await new Promise((resolve,reject)=>{const timeout=setTimeout(()=>reject(Error('動画シークがタイムアウトしました')),15000);video.addEventListener('seeked',()=>{clearTimeout(timeout);resolve();},{once:true});video.currentTime=t;});}
26
  async function sampleInputs(){
@@ -34,51 +36,75 @@ async function sampleInputs(){
34
  const pixels=ctx.getImageData(0,0,288,384).data,n=288*384,data=new Float32Array(n*3);for(let i=0;i<n;i++)for(let c=0;c<3;c++)data[c*n+i]=(pixels[i*4+c]-mean[c])/std[c];inputs.push(data);
35
  }return inputs;
36
  }
37
- function compare(observed,reference,ids){
38
- const xy=[],z=[];let lost=0;
39
- for(let frame=0;frame<observed.length;frame++)for(const i of ids){const ref=reference[frame],out=observed[frame];if(ref.scores[i]<=.3)continue;
40
- xy.push(Math.hypot(out.points[i*3]-ref.points[i*3],out.points[i*3+1]-ref.points[i*3+1]));z.push(Math.abs(out.points[i*3+2]-ref.points[i*3+2]));if(out.scores[i]<=.3)lost++;
41
- }return {observations:xy.length,xyP95_px:pct(xy,.95),zP95_m:pct(z,.95),confidenceLossCount:lost};
42
- }
43
  function render(){
44
  $('results').replaceChildren();for(const r of results){const tr=document.createElement('tr');
45
  const metrics=r.metrics,part=p=>p?.observations?`${p.xyP95_px.toFixed(2)}px / ${(p.zP95_m*1000).toFixed(1)}mm`:'観測不足';
46
- for(const value of[r.id,(r.sizeBytes/1e6).toFixed(1),metrics?`${metrics.p50Ms.toFixed(2)} / ${metrics.p95Ms.toFixed(2)}`:'—',metrics?metrics.aggregateFPS.toFixed(1):'—',metrics?metrics.perInputFPS.toFixed(1):'—',part(r.ankles),part(r.feet),r.error||'推論完了']){const td=document.createElement('td');td.textContent=value;tr.append(td);}if(r.error)tr.className='bad';$('results').append(tr);
 
47
  }
48
  }
 
 
 
 
 
 
 
 
 
 
 
49
  async function measure(){
50
- stopped=false;results=[];render();$('start').disabled=true;$('stop').disabled=false;
51
  try{
52
  const selected=[...document.querySelectorAll('#models input:checked')].map(x=>x.value),set=new Set(selected);
53
  for(const id of selected){const family=catalog.variants.find(v=>v.id===id).model;set.add(family+'-fp32');}
54
  const variants=catalog.variants.filter(v=>set.has(v.id)).toSorted((a,b)=>(a.policy==='fp32'?0:1)-(b.policy==='fp32'?0:1)||a.id.localeCompare(b.id));
55
- const runtime=await loadRuntime(),inputs=await sampleInputs(),reference=new Map(),views=Number($('views').value),iterations=Math.max(12,Math.min(1000,Number($('iterations').value)||40));
56
- for(const variant of variants){if(stopped)break;const record={id:variant.id,sizeBytes:variant.sizeBytes,views,backend:'webgpu',runtime:'1.27.0-JSEP-f32-accumulation-patch',hfRevision:revision};results.push(record);let session;
57
- try{
58
- const data=await bytes(variant.file,variant.sha256);status(`初期化: ${variant.id}`);const load=performance.now();
59
- session=await runtime.InferenceSession.create(data,{executionProviders:['webgpu'],graphOptimizationLevel:'all'});record.loadMs=performance.now()-load;
60
- for(let i=0;i<3;i++){const t=new runtime.Tensor('float32',inputs[i%inputs.length],[1,3,384,288]);const out=await session.run({input:t});Object.values(out).forEach(x=>x.dispose());t.dispose();}
61
- const times=[],observed=[];status(`計測: ${variant.id} · ${views}入力相当`);const started=performance.now();
62
- for(let i=0;i<iterations&&!stopped;i++){
63
- const input=new runtime.Tensor('float32',inputs[i%inputs.length],[1,3,384,288]),t=performance.now();
64
- const out=await session.run({input});const points=out.keypoints.data,scores=out.scores.data;times.push(performance.now()-t);
65
- if(points.length!==399||scores.length!==133||points.some(v=>!Number.isFinite(v))||scores.some(v=>!Number.isFinite(v)))throw Error('全133関節の出力契約または有限値検査に失敗');
66
- if(i<inputs.length)observed.push({points:Array.from(points),scores:Array.from(scores)});Object.values(out).forEach(x=>x.dispose());input.dispose();
67
- }
68
- const elapsed=performance.now()-started;
69
- record.metrics={p50Ms:pct(times,.5),p95Ms:pct(times,.95),aggregateFPS:times.length/elapsed*1000,perInputFPS:times.length/elapsed*1000/views,actualInferences:times.length,wallMs:elapsed,sessionTimesMs:times};
70
- if(variant.policy==='fp32')reference.set(variant.model,observed);
71
- const ref=reference.get(variant.model);if(ref&&observed.length===ref.length){record.body=compare(observed,ref,Array.from({length:23},(_,i)=>i));record.ankles=compare(observed,ref,[15,16]);record.feet=compare(observed,ref,[17,18,19,20,21,22]);}
72
- }catch(e){record.error=String(e.message||e);}finally{if(session)await session.release().catch(()=>{});render();}
73
- }status(stopped?'停止しました':'計測完了。合計fpsと入力あたりfpsを分けて確認してください。');$('download').disabled=false;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
74
  }catch(e){status(String(e.message||e));}finally{$('start').disabled=false;$('stop').disabled=true;}
75
  }
76
  $('video-file').onchange=e=>{if(!e.target.files[0])return;if(videoURL)URL.revokeObjectURL(videoURL);videoURL=URL.createObjectURL(e.target.files[0]);video.src=videoURL;video.load();};
77
  $('start').onclick=measure;$('stop').onclick=()=>{stopped=true;};
78
- $('download').onclick=()=>{const data={schema:'rtmw3d-gpu-comparison/v1',createdAt:new Date().toISOString(),source:{width:video.videoWidth,height:video.videoHeight,duration:video.duration},results,notes:['All variants output all 133 joints','Differences are relative to the same architecture FP32 result, not ground truth','1–4 inputs measure a shared-session workload, not synchronized physical cameras','GPU utilization and VRAM are not measured']};const u=URL.createObjectURL(new Blob([JSON.stringify(data,null,2)],{type:'application/json'})),a=document.createElement('a');a.href=u;a.download='rtmw3d-fullbody-gpu-comparison.json';a.click();setTimeout(()=>URL.revokeObjectURL(u),1000);};
79
  try{
80
  const model=await(await get(`https://huggingface.co/api/models/${REPO}`)).json();revision=model.sha;base=`https://huggingface.co/${REPO}/resolve/${revision}/`;catalog=await(await get(base+'models.json')).json();
81
  if(catalog.keypointCount!==133)throw Error('全身133点のカタログではありません');$('models').replaceChildren();
82
- for(const v of catalog.variants){const label=document.createElement('label'),checkbox=document.createElement('input');checkbox.type='checkbox';checkbox.value=v.id;checkbox.checked=['rtmw3d-l-fp32','rtmw3d-l-fp16-backbone','rtmw3d-l-int8-backbone'].includes(v.id);label.append(checkbox,document.createTextNode(`${v.id} · ${(v.sizeBytes/1e6).toFixed(0)}MB`));$('models').append(label);}
 
83
  $('start').disabled=false;status(`HF revision ${revision}\n全候補:全133点。FP16・INT8の速さを仮定しません。`);
84
  }catch(e){status(String(e.message||e));}
 
1
+ import {MODES,GROUPS,percentile as pct,compare,qualityGate,createExecution,summarizeProfile} from './execution.js';
2
  const $=id=>document.getElementById(id),REPO='RabbitDaisuke/rtmw3d-webgpu-onnx';
3
  const video=$('video'),crop=$('crop'),ctx=crop.getContext('2d',{willReadFrequently:true});
4
+ let catalog,revision,base,ort,stopped=false,results=[],videoURL,runContext;
5
+ const runtimes=new Map();
6
  const status=s=>$('status').textContent=s;
 
7
  async function hash(bytes){return [...new Uint8Array(await crypto.subtle.digest('SHA-256',bytes))].map(x=>x.toString(16).padStart(2,'0')).join('');}
8
  async function get(url){const r=await fetch(url,{credentials:'omit',referrerPolicy:'no-referrer'});if(!r.ok)throw Error(`${r.status}: ${url}`);return r;}
9
  async function bytes(file,sha){
 
12
  status(`取得・照合中: ${file}`);r=await get(key);const b=await r.arrayBuffer();if(await hash(b)!==sha)throw Error(`SHA-256不一致: ${file}`);
13
  try{await cache.put(key,new Response(b));}catch{status('キャッシュ容量不足。取得済みモデルで計測を続けます。');}return b;
14
  }
15
+ async function loadRuntime(flavour='patched'){
16
+ if(runtimes.has(flavour))return runtimes.get(flavour);if(!navigator.gpu)throw Error('WebGPU対応のPCブラウザで開いてください');
17
  const adapter=await navigator.gpu.requestAdapter({powerPreference:'high-performance'});if(!adapter)throw Error('WebGPUアダプターを取得できません');
18
+ const manifest=catalog.runtime,entry=manifest.files.find(f=>f.path===(flavour==='original'?manifest.originalEntrypoint:manifest.entrypoint)),glue=manifest.files.find(f=>f.path.endsWith('.jsep.mjs'));
19
+ if(!entry)throw Error('このHFリビジョンには未パッチJSEPランタイムがありません');
20
  const js=URL.createObjectURL(new Blob([await bytes(entry.path,entry.sha256)],{type:'text/javascript'}));
21
  const module=URL.createObjectURL(new Blob([await bytes(glue.path,glue.sha256)],{type:'text/javascript'}));
22
  ort=await import(js);ort.env.wasm.numThreads=1;ort.env.wasm.proxy=false;
23
  ort.env.wasm.wasmPaths={mjs:module,wasm:base+'runtime/ort-wasm-simd-threaded.jsep.wasm'};
24
+ ort.env.webgpu.powerPreference='high-performance';ort.env.webgpu.profiling={mode:'off',ondata:()=>{}};
25
+ runtimes.set(flavour,ort);return ort;
26
  }
27
  async function seek(t){if(Math.abs(video.currentTime-t)<.001&&video.readyState>=2)return;await new Promise((resolve,reject)=>{const timeout=setTimeout(()=>reject(Error('動画シークがタイムアウトしました')),15000);video.addEventListener('seeked',()=>{clearTimeout(timeout);resolve();},{once:true});video.currentTime=t;});}
28
  async function sampleInputs(){
 
36
  const pixels=ctx.getImageData(0,0,288,384).data,n=288*384,data=new Float32Array(n*3);for(let i=0;i<n;i++)for(let c=0;c<3;c++)data[c*n+i]=(pixels[i*4+c]-mean[c])/std[c];inputs.push(data);
37
  }return inputs;
38
  }
 
 
 
 
 
 
39
  function render(){
40
  $('results').replaceChildren();for(const r of results){const tr=document.createElement('tr');
41
  const metrics=r.metrics,part=p=>p?.observations?`${p.xyP95_px.toFixed(2)}px / ${(p.zP95_m*1000).toFixed(1)}mm`:'観測不足';
42
+ const verdict=r.error||(r.reference?'基準':r.qualityPassed===true?'差分基準内':r.qualityPassed===false?'差分基準外':'計測中');
43
+ for(const value of[r.id,r.executionLabel,(r.sizeBytes/1e6).toFixed(1),metrics?`${metrics.p50Ms.toFixed(2)} / ${metrics.p95Ms.toFixed(2)}`:'—',metrics?metrics.aggregateFPS.toFixed(1):'—',metrics?metrics.perInputFPS.toFixed(1):'—',part(r.body),part(r.ankles),part(r.feet),verdict]){const td=document.createElement('td');td.textContent=value;tr.append(td);}if(r.error||r.qualityPassed===false)tr.className='bad';$('results').append(tr);
44
  }
45
  }
46
+ async function profile(runtime,data,mode,inputs){
47
+ const events=[];let runner;
48
+ runtime.env.webgpu.profiling={mode:'default',ondata:e=>events.push(e)};
49
+ try{
50
+ runner=await createExecution(runtime,data,mode);
51
+ for(let i=0;i<3;i++)await runner.run(inputs[i%inputs.length]);
52
+ await runner.device.queue.onSubmittedWorkDone();await new Promise(resolve=>setTimeout(resolve,0));events.length=0;
53
+ await runner.run(inputs[0]);await runner.device.queue.onSubmittedWorkDone();await new Promise(resolve=>setTimeout(resolve,0));
54
+ return summarizeProfile(events);
55
+ }finally{if(runner)await runner.release();runtime.env.webgpu.profiling={mode:'off',ondata:()=>{}};}
56
+ }
57
  async function measure(){
58
+ stopped=false;results=[];render();$('start').disabled=true;$('stop').disabled=false;$('download').disabled=true;
59
  try{
60
  const selected=[...document.querySelectorAll('#models input:checked')].map(x=>x.value),set=new Set(selected);
61
  for(const id of selected){const family=catalog.variants.find(v=>v.id===id).model;set.add(family+'-fp32');}
62
  const variants=catalog.variants.filter(v=>set.has(v.id)).toSorted((a,b)=>(a.policy==='fp32'?0:1)-(b.policy==='fp32'?0:1)||a.id.localeCompare(b.id));
63
+ const requestedModes=[...document.querySelectorAll('#modes input:checked')].map(x=>x.value);
64
+ if(!selected.length||!requestedModes.length)throw Error('モデルと実行経路を選択してください');
65
+ const modes=MODES.filter(m=>m.id==='standard'||requestedModes.includes(m.id));
66
+ const runtime=await loadRuntime(),inputs=await sampleInputs(),reference=new Map(),views=Number($('views').value),iterations=Math.max(12,Math.min(1000,Number($('iterations').value)||200));
67
+ runContext={source:{width:video.videoWidth,height:video.videoHeight,duration:video.duration},inputShape:[1,3,384,288],sampleFractions:[.2,.5,.8],roiPercent:Object.fromEntries(['x','y','w','h'].map(k=>[k,Number($(k).value)])),views,iterations,warmupRuns:5};
68
+ for(const variant of variants){
69
+ if(stopped)break;const data=await bytes(variant.file,variant.sha256);
70
+ const routes=modes.map(mode=>({mode,flavour:'patched'}));
71
+ if($('original').checked&&variant.policy==='fp32')routes.push({mode:MODES[0],flavour:'original'});
72
+ for(const {mode,flavour} of routes){
73
+ if(stopped)break;
74
+ const record={id:variant.id,sizeBytes:variant.sizeBytes,views,requestedBackend:'webgpu',executionMode:mode.id,executionLabel:mode.label+(flavour==='original'?' / 未パッチJSEP':''),preferredLayout:mode.layout,graphCaptureRequested:mode.capture,cpuFallbackDisabled:mode.capture,runtime:flavour==='original'?'1.27.0-JSEP-original':'1.27.0-JSEP-f32-accumulation-patch',hfRevision:revision};
75
+ results.push(record);let runner;
76
+ try{
77
+ status(`初期化: ${variant.id} / ${record.executionLabel}`);const load=performance.now(),activeRuntime=await loadRuntime(flavour);
78
+ activeRuntime.env.webgpu.profiling={mode:'off',ondata:()=>{}};
79
+ runner=await createExecution(activeRuntime,data,mode);record.loadMs=performance.now()-load;
80
+ record.enabledGPUFeatures=[...runner.device.features];
81
+ for(let i=0;i<5;i++)await runner.run(inputs[i%inputs.length]);
82
+ const times=[],submitTimes=[],readbacks=[],observed=[];status(`計測: ${variant.id} / ${mode.label}`);const started=performance.now();
83
+ for(let i=0;i<iterations&&!stopped;i++){
84
+ const out=await runner.run(inputs[i%inputs.length]);times.push(out.totalMs);submitTimes.push(out.runMs);readbacks.push(out.readbackMs);
85
+ if(i<inputs.length)observed.push({points:Array.from(out.points),scores:Array.from(out.scores)});
86
+ }
87
+ const elapsed=performance.now()-started;record.interrupted=stopped;
88
+ if(times.length)record.metrics={p50Ms:pct(times,.5),p95Ms:pct(times,.95),aggregateFPS:times.length/elapsed*1000,perInputFPS:times.length/elapsed*1000/views,actualInferences:times.length,wallMs:elapsed,sessionTimesMs:times,submitP50Ms:pct(submitTimes,.5),readbackP50Ms:pct(readbacks,.5),includesUploadAndReadback:true,profilingEnabledDuringTiming:false};
89
+ record.reference=variant.policy==='fp32'&&mode.id==='standard'&&flavour==='patched';
90
+ if(record.reference&&!stopped)reference.set(variant.model,observed);
91
+ const ref=reference.get(variant.model);
92
+ if(ref&&observed.length===ref.length&&!stopped){for(const [name,ids]of Object.entries(GROUPS))record[name]=compare(observed,ref,ids);record.qualityPassed=qualityGate(record);}
93
+ await runner.release();runner=null;
94
+ if($('profile').checked&&!stopped){status(`演算別診断: ${variant.id} / ${record.executionLabel}`);try{record.gpuProfile=await profile(activeRuntime,data,mode,inputs);}catch(error){record.profilingError=String(error.message||error);}}
95
+ }catch(e){record.error=String(e.message||e);}finally{if(runner)await runner.release().catch(()=>{});render();}
96
+ }
97
+ }
98
+ status(stopped?'停止しました':'計測完了。GPU完了と座標の読戻しを含む時間です。Graph Capture失敗は結果欄に記録します。');$('download').disabled=false;
99
  }catch(e){status(String(e.message||e));}finally{$('start').disabled=false;$('stop').disabled=true;}
100
  }
101
  $('video-file').onchange=e=>{if(!e.target.files[0])return;if(videoURL)URL.revokeObjectURL(videoURL);videoURL=URL.createObjectURL(e.target.files[0]);video.src=videoURL;video.load();};
102
  $('start').onclick=measure;$('stop').onclick=()=>{stopped=true;};
103
+ $('download').onclick=()=>{const data={schema:'rtmw3d-gpu-comparison/v2',createdAt:new Date().toISOString(),source:runContext?.source,runContext,results,notes:['All variants output all 133 joints','Differences are relative to the architecture FP32 standard-path result, not ground truth','Timings include input upload, GPU completion and fresh 133-joint CPU readback','GPU timestamp profiling uses a separate session and is excluded from speed measurements','requestedBackend is the requested provider; it does not prove that every node ran on GPU','1–4 inputs measure a shared-session workload, not synchronized physical cameras','GPU utilization, VRAM, device identity, video names and video contents are not exported']};const u=URL.createObjectURL(new Blob([JSON.stringify(data,null,2)],{type:'application/json'})),a=document.createElement('a');a.href=u;a.download='rtmw3d-fullbody-gpu-comparison.json';a.click();setTimeout(()=>URL.revokeObjectURL(u),1000);};
104
  try{
105
  const model=await(await get(`https://huggingface.co/api/models/${REPO}`)).json();revision=model.sha;base=`https://huggingface.co/${REPO}/resolve/${revision}/`;catalog=await(await get(base+'models.json')).json();
106
  if(catalog.keypointCount!==133)throw Error('全身133点のカタログではありません');$('models').replaceChildren();
107
+ for(const v of catalog.variants){const label=document.createElement('label'),checkbox=document.createElement('input');checkbox.type='checkbox';checkbox.value=v.id;checkbox.checked=v.id==='rtmw3d-l-fp32';label.append(checkbox,document.createTextNode(`${v.id} · ${(v.sizeBytes/1e6).toFixed(0)}MB`));$('models').append(label);}
108
+ for(const m of MODES){const label=document.createElement('label'),checkbox=document.createElement('input');checkbox.type='checkbox';checkbox.value=m.id;checkbox.checked=true;label.append(checkbox,document.createTextNode(m.label));$('modes').append(label);}
109
  $('start').disabled=false;status(`HF revision ${revision}\n全候補:全133点。FP16・INT8の速さを仮定しません。`);
110
  }catch(e){status(String(e.message||e));}
benchmark/execution.js ADDED
@@ -0,0 +1,83 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ export const MODES = [
2
+ {id:'standard',label:'標準 NCHW',layout:'NCHW',gpuIO:false,capture:false},
3
+ {id:'gpu-io',label:'GPUバッファ再利用 NCHW',layout:'NCHW',gpuIO:true,capture:false},
4
+ {id:'nhwc',label:'GPUバッファ再利用 NHWC',layout:'NHWC',gpuIO:true,capture:false},
5
+ {id:'capture',label:'Graph Capture NHWC',layout:'NHWC',gpuIO:true,capture:true},
6
+ ];
7
+ export const GROUPS={body:Array.from({length:23},(_,i)=>i),hips:[11,12],knees:[13,14],ankles:[15,16],feet:[17,18,19,20,21,22]};
8
+ export function percentile(values,p){const a=[...values].sort((x,y)=>x-y);return a.length?a[Math.floor((a.length-1)*p)]:null;}
9
+ export function compare(observed,reference,ids){
10
+ const xy=[],z=[];let lost=0;
11
+ for(let frame=0;frame<observed.length;frame++)for(const i of ids){
12
+ const ref=reference[frame],out=observed[frame];if(ref.scores[i]<=.3)continue;
13
+ xy.push(Math.hypot(out.points[i*3]-ref.points[i*3],out.points[i*3+1]-ref.points[i*3+1]));
14
+ z.push(Math.abs(out.points[i*3+2]-ref.points[i*3+2]));if(out.scores[i]<=.3)lost++;
15
+ }
16
+ return {observations:xy.length,xyP95_px:percentile(xy,.95),zP95_m:percentile(z,.95),confidenceLossCount:lost};
17
+ }
18
+ export function qualityGate(parts){
19
+ return Object.keys(GROUPS).every(name=>{const p=parts[name];return p?.observations>0&&p.xyP95_px<=3&&p.zP95_m<=.05&&p.confidenceLossCount===0;});
20
+ }
21
+ export function validateOutput(points,scores){
22
+ if(points.length!==399||scores.length!==133||points.some(v=>!Number.isFinite(v))||scores.some(v=>!Number.isFinite(v)))throw Error('全133関節の出力契約または有限値検査に失敗');
23
+ }
24
+ // Fixed GPU input/output addresses are required by graph replay. Read fresh
25
+ // buffers directly: Tensor.getData() changes its location to CPU and caches data.
26
+ export async function createExecution(runtime,modelBytes,mode){
27
+ const options={executionProviders:[{name:'webgpu',preferredLayout:mode.layout}],graphOptimizationLevel:'all'};
28
+ if(mode.gpuIO)options.preferredOutputLocation='gpu-buffer';
29
+ if(mode.capture){options.enableGraphCapture=true;options.extra={session:{disable_cpu_ep_fallback:'1'}};}
30
+ let session,device,input,fetches,inputBuffer,staging;
31
+ const buffers=[];
32
+ const release=async()=>{
33
+ try{if(session)await session.release();}
34
+ finally{session=null;input?.dispose();input=null;for(const t of Object.values(fetches??{}))t.dispose();fetches=null;for(const b of buffers.splice(0))b.destroy();}
35
+ };
36
+ try{
37
+ session=await runtime.InferenceSession.create(modelBytes,options);
38
+ device=await runtime.env.webgpu.device;
39
+ if(mode.gpuIO){
40
+ const make=(size,usage)=>{const b=device.createBuffer({size:Math.ceil(size/16)*16,usage});buffers.push(b);return b;};
41
+ const usage=GPUBufferUsage.STORAGE|GPUBufferUsage.COPY_SRC|GPUBufferUsage.COPY_DST;
42
+ inputBuffer=make(3*384*288*4,usage);
43
+ input=runtime.Tensor.fromGpuBuffer(inputBuffer,{dataType:'float32',dims:[1,3,384,288]});
44
+ fetches={
45
+ keypoints:runtime.Tensor.fromGpuBuffer(make(399*4,usage),{dataType:'float32',dims:[1,133,3]}),
46
+ scores:runtime.Tensor.fromGpuBuffer(make(133*4,usage),{dataType:'float32',dims:[1,133]}),
47
+ };
48
+ staging=make(532*4,GPUBufferUsage.COPY_DST|GPUBufferUsage.MAP_READ);
49
+ }
50
+ return {session,device,release,async run(data){
51
+ const started=performance.now();let points,scores,runMs,readbackMs=0;
52
+ if(mode.gpuIO){
53
+ device.queue.writeBuffer(inputBuffer,0,data);
54
+ await session.run({input},fetches);runMs=performance.now()-started;
55
+ const read=performance.now(),encoder=device.createCommandEncoder();
56
+ encoder.copyBufferToBuffer(fetches.keypoints.gpuBuffer,0,staging,0,399*4);
57
+ encoder.copyBufferToBuffer(fetches.scores.gpuBuffer,0,staging,399*4,133*4);
58
+ device.queue.submit([encoder.finish()]);
59
+ await staging.mapAsync(GPUMapMode.READ);
60
+ try{const packed=new Float32Array(staging.getMappedRange().slice(0));points=packed.slice(0,399);scores=packed.slice(399);}
61
+ finally{staging.unmap();}
62
+ readbackMs=performance.now()-read;
63
+ }else{
64
+ const tensor=new runtime.Tensor('float32',data,[1,3,384,288]);let out;
65
+ try{out=await session.run({input:tensor});points=out.keypoints.data.slice();scores=out.scores.data.slice();runMs=performance.now()-started;}
66
+ finally{for(const t of Object.values(out??{}))t.dispose();tensor.dispose();}
67
+ }
68
+ validateOutput(points,scores);
69
+ return {points,scores,totalMs:performance.now()-started,runMs,readbackMs};
70
+ }};
71
+ }catch(error){await release().catch(()=>{});throw error;}
72
+ }
73
+ export function summarizeProfile(events){
74
+ const entries=new Map();let totalNs=0;
75
+ for(const e of events){
76
+ const duration=e.endTime-e.startTime;if(!Number.isFinite(duration)||duration<0)continue;
77
+ totalNs+=duration;const key=[e.kernelType,e.kernelName,e.programName].join('|');
78
+ if(!entries.has(key))entries.set(key,{kernelType:e.kernelType,kernelName:e.kernelName,programName:e.programName,dispatches:0,totalMs:0});
79
+ const item=entries.get(key);item.dispatches++;item.totalMs+=duration/1e6;
80
+ }
81
+ return {available:events.length>0,dispatches:events.length,summedKernelMs:totalNs/1e6,topKernels:[...entries.values()].sort((a,b)=>b.totalMs-a.totalMs).slice(0,25),
82
+ note:'Separate instrumented run; summed dispatch durations do not include CPU execution, transfers or queue gaps.'};
83
+ }
benchmark/index.html CHANGED
@@ -1,6 +1,6 @@
1
  <!doctype html><html lang="ja"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>全身RTMW3D · GPU比較</title><style>
2
  *{box-sizing:border-box}body{max-width:1280px;margin:32px auto;padding:0 22px;background:#f5f5f0;color:#20352c;font:14px/1.7 system-ui,sans-serif}h1{font-size:26px}section{background:#fff;border:1px solid #dce4d8;border-radius:9px;padding:20px;margin:16px 0}button,select,input{font:inherit;padding:8px}button{background:#194f40;color:white;border:0;border-radius:5px;cursor:pointer}button:disabled{opacity:.4;cursor:default}label{display:inline-flex;gap:7px;align-items:center;margin:6px 18px 6px 0}input[type=number]{width:76px;border:1px solid #cbd5c5;border-radius:4px}video{max-height:280px;width:100%;background:#102a23}table{border-collapse:collapse;width:100%;font-size:12px}td,th{padding:9px;border-bottom:1px solid #e3e8df;text-align:left}#models{display:grid;grid-template-columns:repeat(auto-fit,minmax(280px,1fr))}#status{white-space:pre-wrap;overflow-wrap:anywhere;font-size:12px}small{color:#718168}a{color:#286951}.table-wrap{overflow-x:auto}.bad{color:#a04c33}canvas{display:none}.note{font-size:12px;color:#6e7f65}</style><script type="module" src="./benchmark.js"></script></head><body>
3
- <h1>足首・足先を含む全身RTMW3DのWebGPU比較</h1><p>全候補は133点を保持します。ネットワーク内の量子化位置と数値形式を変えて、同じ動画の同じ入力で速度と関節の変化を測定します。</p><p class="note">1〜4入力は1セッションを時分割する推論負荷です。物理カメラの同期・三角測量・SteamVR送出の実測ではありません。映像は端末から送信しません。</p>
4
  <section><h2>1. ローカル動画と人物範囲</h2><input id="video-file" type="file" accept="video/*"><video id="video" controls playsinline muted></video><div><label>ROI左 % <input id="x" type="number" value="0" min="0" max="99"></label><label>ROI上 % <input id="y" type="number" value="0" min="0" max="99"></label><label>ROI幅 % <input id="w" type="number" value="100" min="1" max="100"></label><label>ROI高 % <input id="h" type="number" value="100" min="1" max="100"></label></div><small>全身と足先が入る範囲を指定してください。3時刻の同じROIを各モデルで使用します。解像度はモデル入力288×384にだけ変換します。</small></section>
5
- <section><h2>2. 比較候補</h2><div id="models">カタログ取得中…</div><p class="note">同じLまたはXのFP32基準も自動で測定します。FP32との差は正解データに対する精度ではありません。初回は選択したモデルだけ取得・キャッシュします。</p><label>入力相当数 <select id="views"><option>1</option><option>2</option><option>3</option><option>4</option></select></label><label>計測推論回数 <input id="iterations" type="number" value="40" min="12" max="1000"></label><button id="start" disabled>選択した全身モデルを比較</button> <button id="stop" disabled>停止</button> <button id="download" disabled>結果JSON</button><pre id="status" role="status"></pre></section>
6
- <section class="table-wrap"><table><thead><tr><th>全身モデル</th><th>容量 MB</th><th>p50 / p95 ms</th><th>合計fps</th><th>入力あたりfps</th><th>足首 XY / Z p95</th><th>足先 XY / Z p95</th><th>判定</th></tr></thead><tbody id="results"></tbody></table></section><p><a href="https://github.com/open-mmlab/mmpose" target="_blank" rel="noreferrer">元モデル:OpenMMLab MMPose</a> · <a href="https://huggingface.co/RabbitDaisuke/rtmw3d-webgpu-onnx" target="_blank" rel="noreferrer">モデル・比較レポート</a></p><canvas id="crop" width="288" height="384"></canvas></body></html>
 
1
  <!doctype html><html lang="ja"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>全身RTMW3D · GPU比較</title><style>
2
  *{box-sizing:border-box}body{max-width:1280px;margin:32px auto;padding:0 22px;background:#f5f5f0;color:#20352c;font:14px/1.7 system-ui,sans-serif}h1{font-size:26px}section{background:#fff;border:1px solid #dce4d8;border-radius:9px;padding:20px;margin:16px 0}button,select,input{font:inherit;padding:8px}button{background:#194f40;color:white;border:0;border-radius:5px;cursor:pointer}button:disabled{opacity:.4;cursor:default}label{display:inline-flex;gap:7px;align-items:center;margin:6px 18px 6px 0}input[type=number]{width:76px;border:1px solid #cbd5c5;border-radius:4px}video{max-height:280px;width:100%;background:#102a23}table{border-collapse:collapse;width:100%;font-size:12px}td,th{padding:9px;border-bottom:1px solid #e3e8df;text-align:left}#models{display:grid;grid-template-columns:repeat(auto-fit,minmax(280px,1fr))}#status{white-space:pre-wrap;overflow-wrap:anywhere;font-size:12px}small{color:#718168}a{color:#286951}.table-wrap{overflow-x:auto}.bad{color:#a04c33}canvas{display:none}.note{font-size:12px;color:#6e7f65}</style><script type="module" src="./benchmark.js"></script></head><body>
3
+ <h1>足首・足先を含む全身RTMW3DのWebGPU比較</h1><p>全候補は133点を保持します。同じモデルと動画入力で、数値形式・GPUメモリ再利用・実行経路を比較します。</p><p class="note">1〜4入力は1セッションを時分割する推論負荷です。物理カメラの同期・三角測量・SteamVR送出の実測ではありません。映像は端末から送信しません。</p><label><input id="original" type="checkbox" checked>FP32で未パッチJSEP版の標準経路も比較する</label><p class="note">同じORT 1.27.0 JSEPのパッチ前後を比較します。別実装のwebgpu-only配布物とは区別します。</p>
4
  <section><h2>1. ローカル動画と人物範囲</h2><input id="video-file" type="file" accept="video/*"><video id="video" controls playsinline muted></video><div><label>ROI左 % <input id="x" type="number" value="0" min="0" max="99"></label><label>ROI上 % <input id="y" type="number" value="0" min="0" max="99"></label><label>ROI幅 % <input id="w" type="number" value="100" min="1" max="100"></label><label>ROI高 % <input id="h" type="number" value="100" min="1" max="100"></label></div><small>全身と足先が入る範囲を指定してください。3時刻の同じROIを各モデルで使用します。解像度はモデル入力288×384にだけ変換します。</small></section>
5
+ <section><h2>2. 比較候補</h2><div id="models">カタログ取得中…</div><p class="note">同じLまたはXのFP32基準も自動で測定します。FP32との差は正解データに対する精度ではありません。初回は選択したモデルだけ取得・キャッシュします。</p><h2>3. 実行経路</h2><div id="modes"></div><p class="note">標準経路も基準として自動測定します。GPUバッファ再利用とGraph Captureも、入力転送・GPU完了・133関節の読戻しを含めて測ります。Graph Captureで未対応演算があれば失敗理由を記録します。差分基準は身体・股関節・膝・足首・足先それぞれXY p95 ≤ 3px、Z p95 ≤ 5cm、信頼度低下なしです。観測不足では合格にしません。</p><label><input id="profile" type="checkbox" checked>計測後に別セッションで演算別GPU診断を記録</label><p class="note">診断にはtimestamp-queryが必要です。診断時の負荷は速度計測へ含めません。JSONに動画名・映像・端末識別情報は保存しません。</p><label>入力相当数 <select id="views"><option>1</option><option>2</option><option>3</option><option>4</option></select></label><label>計測推論回数 <input id="iterations" type="number" value="200" min="12" max="1000"></label><button id="start" disabled>選択した全身モデルを比較</button> <button id="stop" disabled>停止</button> <button id="download" disabled>結果JSON</button><pre id="status" role="status"></pre></section>
6
+ <section class="table-wrap"><table><thead><tr><th>全身モデル</th><th>実行経路</th><th>容量 MB</th><th>p50 / p95 ms</th><th>合計fps</th><th>入力あたりfps</th><th>身体・足 XY / Z p95</th><th>足首 XY / Z p95</th><th>足先 XY / Z p95</th><th>判定</th></tr></thead><tbody id="results"></tbody></table></section><p><a href="https://github.com/open-mmlab/mmpose" target="_blank" rel="noreferrer">元モデル:OpenMMLab MMPose</a> · <a href="https://huggingface.co/RabbitDaisuke/rtmw3d-webgpu-onnx" target="_blank" rel="noreferrer">モデル・比較レポート</a></p><canvas id="crop" width="288" height="384"></canvas></body></html>