Update browser execution-path benchmark f582bf55550ed0b5efbb21e8606f75d80918ca31
Browse files- benchmark/benchmark.js +62 -36
- benchmark/execution.js +83 -0
- benchmark/index.html +3 -3
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(
|
| 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 |
-
|
|
|
|
| 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
|
| 56 |
-
|
| 57 |
-
|
| 58 |
-
|
| 59 |
-
|
| 60 |
-
|
| 61 |
-
|
| 62 |
-
|
| 63 |
-
|
| 64 |
-
|
| 65 |
-
|
| 66 |
-
|
| 67 |
-
|
| 68 |
-
|
| 69 |
-
|
| 70 |
-
|
| 71 |
-
|
| 72 |
-
|
| 73 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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/
|
| 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=
|
|
|
|
| 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点を保持します。
|
| 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="
|
| 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>
|