quantid commited on
Commit
f2fdb95
·
verified ·
1 Parent(s): 4973d04

대조 가로축을 동시성(9%만 공개)에서 모델 크기(100% 공개)로 교체 — 남의 기록이 실제로 찍히도록

Browse files
Files changed (1) hide show
  1. index.html +83 -98
index.html CHANGED
@@ -206,6 +206,7 @@ svg{display:block; max-width:100%; height:auto}
206
 
207
  .board{display:grid;grid-template-columns:minmax(260px,340px) 1fr;gap:20px;margin-top:30px;align-items:start}
208
  .bpanel .f2{display:grid;grid-template-columns:1fr 1fr;gap:12px}
 
209
  .bpanel select,.bpanel input{width:100%;padding:10px 12px;border:1px solid var(--line2);
210
  background:var(--surface);color:var(--ink);font:inherit;font-size:14px;border-radius:0}
211
  .bpanel select:focus,.bpanel input:focus{outline:2px solid var(--blue);outline-offset:-2px}
@@ -615,9 +616,11 @@ table.brank tr.mine td{font-weight:600}
615
  <div class="panel bpanel">
616
  <div class="field"><label for="bGpu">쓰시는 GPU</label>
617
  <select id="bGpu"></select></div>
618
- <div class="f2">
619
  <div class="field"><label for="bQ">양자화</label><select id="bQ"></select></div>
620
- <div class="field"><label for="bAct">활성 파라미터 (십억)</label>
 
 
621
  <input id="bAct" type="number" value="3" min="0.1" step="0.1"></div>
622
  </div>
623
  <div class="f2">
@@ -662,7 +665,7 @@ table.brank tr.mine td{font-weight:600}
662
  <div class="note" id="bNote"><p>GPU를 고르면 계산합니다.</p></div>
663
 
664
  <div class="tw"><table class="brank">
665
- <thead><tr><th>#</th><th>모델</th><th>잰 곳</th><th class="n">동시</th>
666
  <th class="n">GPU당 tok/s</th><th>등급</th></tr></thead>
667
  <tbody id="bTop"></tbody>
668
  </table></div>
@@ -879,77 +882,69 @@ ${msg}`);
879
  $('sendMsg').textContent = '메일 창이 열립니다. 열리지 않으면 contact@quantid.ai 로 보내주십시오.';
880
  });
881
 
882
- /* ===== 09 · 대조 — 업계 395건 위에 귀사 위치 ===== */
883
  (function(){
884
  var B=null, $b=function(i){return document.getElementById(i)};
885
- var W=560,H=400, L=58,R=14,T=18,Bm=44; /* plot 여백 */
886
  var FX=function(n){return n.toLocaleString('ko-KR')};
887
 
888
- fetch('board.json').then(function(r){return r.json()}).then(function(d){
889
- B=d; init();
890
- }).catch(function(e){
891
- $b('bNote').innerHTML='<p>대조 자료를 불러오지 못했습니다.</p>';
892
- });
893
 
894
  function init(){
895
  var gs=Object.keys(B.hw).filter(function(k){
896
- return B.records.some(function(r){return r.g===k});
897
- }).sort();
898
- $b('bGpu').innerHTML='<option value="">— 고르십시오 —</option>'+gs.map(function(k){
899
  var n=B.records.filter(function(r){return r.g===k}).length;
900
- return '<option value="'+k+'">'+k+' ('+n+'건)</option>';
901
- }).join('');
902
- var best=gs.slice().sort(function(a,b){return B.records.filter(function(r){return r.g===b}).length-B.records.filter(function(r){return r.g===a}).length})[0];
903
- $b('bGpu').value = best;
904
 
905
- var qs=Object.keys(B.bpp);
906
- $b('bQ').innerHTML=qs.map(function(q){
907
- return '<option value="'+q+'"'+(q==='FP8'?' selected':'')+'>'+q+'</option>';
908
- }).join('');
909
 
910
- $b('bMeta').textContent='· 레코드 '+B.records.length+'건 · 가속기 '+Object.keys(B.hw).length+'종 · 기준일 '+B.updated;
 
911
  $b('bSrc').textContent='전력은 제조사가 적어둔 정격이라 실제 쓰는 양보다 넉넉하게 잡힙니다. CPU와 냉각은 안 들어갔습니다. '+
912
  '클라우드 값은 '+(B.src.price_by||[]).join(' · ')+'가 '+B.src.as_of+'에 내건 정가고, 약정하면 이보다 쌉니다. '+
913
- '둘 다 견적이 아니라 서로 견주는 잣대로 봐주십시오.';
 
914
 
915
- ['bGpu','bQ','bAct','bCon','bGn','bMon','bOut','bKwh'].forEach(function(id){
916
- $b(id).addEventListener('input', calc);
917
- $b(id).addEventListener('change', calc);
918
- });
919
  calc();
920
  }
921
 
922
- /* 물리 상한: 디코드 1스텝은 활성 가중치를 한 번 읽는다
923
- tok/s ≤ 동시성 × 대역폭 ÷ 활성바이트 */
924
  function ceil(hw, actB, q, con){
925
  if(!hw||!hw.bw||!actB) return null;
926
- var bytes = actB*1e9*(B.bpp[q]||1);
927
- return con * (hw.bw*1e12) / bytes;
928
  }
929
 
930
  function calc(){
931
  var g=$b('bGpu').value; if(!g||!B) return;
932
  var hw=B.hw[g], q=$b('bQ').value,
933
- act=+$b('bAct').value||1, con=+$b('bCon').value||1,
934
- gn=Math.max(1,+$b('bGn').value||1),
935
- mon=+$b('bMon').value||1, out=+$b('bOut').value||1,
936
- kwh=+$b('bKwh').value||0;
937
 
938
  var peers=B.records.filter(function(r){return r.g===g});
939
- var cap = ceil(hw, act, q, con);
940
 
941
- /* 같은 가속기·비슷한 동시성의 실측 중앙값을 귀사 예상 처리량으로 쓴다 */
942
- var near=peers.filter(function(r){return r.c && r.c>=con/4 && r.c<=con*4});
943
- var basis = near.length? near : peers;
944
- var med=null;
945
  if(basis.length){
946
  var a=basis.map(function(r){return r.pg}).sort(function(x,y){return x-y});
947
- med=a[Math.floor(a.length/2)];
948
  }
949
- var est = med;
950
- if(est && cap) est=Math.min(est, cap); /* 물리 상한을 넘지 않게 */
951
 
952
- draw(peers, g, cap, con, est);
953
 
954
  if(!est){
955
  $b('bNote').innerHTML='<p>이 GPU는 기록이 모자라서 추정하지 않겠습니다. 없는 숫자를 지어내는 것보다 낫습니다.</p>';
@@ -957,95 +952,85 @@ ${msg}`);
957
  return;
958
  }
959
 
960
- /* 순위 — GPU당 처리량 기준 백분위 */
961
  var better=peers.filter(function(r){return r.pg>est}).length;
962
- var pct=Math.max(1,Math.round((better+1)/peers.length*100));
963
- $b('kRank').textContent='상위 '+pct+'%';
964
  $b('kRankSub').textContent=g+' 기록 '+peers.length+'건 가운데';
965
 
966
- /* 필요 장수 */
967
- var needTok = mon*out;
968
- var perGpuMonth = est*3600*24*30.4;
969
- var need=Math.max(1, Math.ceil(needTok/perGpuMonth));
970
  $b('kGpu').textContent=FX(need)+'장';
971
  $b('kGpuSub').textContent='한 달 '+FX(Math.round(needTok/1e6))+'백만 토큰을 넘기려면';
972
 
973
- /* 전력 */
974
- if(hw.w){
975
- var kW=need*hw.w/1000, mWh=kW*730;
976
  $b('kPow').textContent=FX(Math.round(mWh*kwh))+'원';
977
  $b('kPowSub').textContent=FX(Math.round(mWh))+' kWh · '+kW.toFixed(1)+' kW';
978
  } else { $b('kPow').textContent='—'; $b('kPowSub').textContent='이 GPU는 정격이 공개 안 됨'; }
979
 
980
- /* 클라우드 */
981
- if(hw.usd){
982
- var usd=need*hw.usd*730;
983
  $b('kUsd').textContent=FX(Math.round(usd*1380/1e4))+'만원';
984
  $b('kUsdSub').textContent='$'+FX(Math.round(usd))+' · 환율 1,380원';
985
  } else { $b('kUsd').textContent='—'; $b('kUsdSub').textContent='공개된 시간당 단가가 없음'; }
986
 
987
- var over = cap && med && med>cap;
988
  $b('bNote').innerHTML='<p>이 설정이면 <strong>GPU 한 장에 초당 '+FX(Math.round(est))+'토큰</strong>쯤 나옵니다. '+
989
- g+' 기록 '+basis.length+'건의 한가운데 값에서 뽑았고'+(over?', 물리적으로 불가능한 만큼은 잘라냈습니다':'')+'. '+
990
- '<b>직접 잰 게 아니라 남의 기록에 견줘 본 값</b>이라, 모델이나 서버 설정이 다르면 달라집니다. '+
 
 
991
  '도입 검토하실 때 쓰시는 장비에서 직접 재 드립니다.'+
992
  (cap?' 참고로 이 조합이 물리적으로 넘을 수 없는 선은 초당 '+FX(Math.round(cap))+'토큰입니다.':'')+'</p>';
993
 
994
- var top=peers.slice().sort(function(a,b){return b.pg-a.pg}).slice(0,8);
995
- $b('bTop').innerHTML=top.map(function(r,i){
996
- return '<tr'+(r.own?' class="mine"':'')+'><td>'+(i+1)+'</td><td>'+(r.m||'—')+
997
- '</td><td>'+(r.by||'—')+'</td><td class="n">'+(r.c||'—')+
998
- '</td><td class="n">'+FX(r.pg)+'</td><td><span class="gr g-'+(r.gr||'')+'">'+(r.gr||'—')+'</span></td></tr>';
999
- }).join('');
1000
  }
1001
 
1002
- function draw(peers, g, cap, con, est){
1003
- var xs=peers.map(function(r){return r.c||1}).concat([con]),
1004
- ys=peers.map(function(r){return r.pg}).concat(est?[est]:[]);
1005
- var x0=Math.max(1,Math.min.apply(null,xs)), x1=Math.max.apply(null,xs),
 
 
1006
  y0=Math.max(.5,Math.min.apply(null,ys)), y1=Math.max.apply(null,ys);
1007
  x1=Math.max(x1,x0*2); y1=Math.max(y1,y0*2);
1008
- var lx=function(v){return L+(Math.log10(Math.max(v,x0))-Math.log10(x0))/(Math.log10(x1)-Math.log10(x0))*(W-L-R)},
1009
- ly=function(v){return H-Bm-(Math.log10(Math.max(v,y0))-Math.log10(y0))/(Math.log10(y1)-Math.log10(y0))*(H-T-Bm)};
1010
 
1011
- var s='<g font-family="IBM Plex Sans KR, sans-serif">';
1012
- /* 격자 */
1013
- for(var p=Math.ceil(Math.log10(y0)); p<=Math.floor(Math.log10(y1)); p++){
1014
- var yy=ly(Math.pow(10,p));
1015
  s+='<line x1="'+L+'" y1="'+yy+'" x2="'+(W-R)+'" y2="'+yy+'" stroke="#DCD6C9" stroke-dasharray="2 3"/>'+
1016
  '<text x="'+(L-7)+'" y="'+(yy+4)+'" text-anchor="end" font-size="9.5" fill="#8A8F98">'+
1017
- (Math.pow(10,p)>=1000?(Math.pow(10,p)/1000)+'k':Math.pow(10,p))+'</text>';
1018
  }
1019
- for(var q2=Math.ceil(Math.log10(x0)); q2<=Math.floor(Math.log10(x1)); q2++){
1020
- var xx=lx(Math.pow(10,q2));
1021
- s+='<text x="'+xx+'" y="'+(H-Bm+16)+'" text-anchor="middle" font-size="9.5" fill="#8A8F98">'+Math.pow(10,q2)+'</text>';
1022
  }
1023
  s+='<line x1="'+L+'" y1="'+(H-Bm)+'" x2="'+(W-R)+'" y2="'+(H-Bm)+'" stroke="#C4BCAB"/>'+
1024
  '<line x1="'+L+'" y1="'+T+'" x2="'+L+'" y2="'+(H-Bm)+'" stroke="#C4BCAB"/>'+
1025
- '<text x="'+((W+L)/2)+'" y="'+(H-8)+'" text-anchor="middle" font-size="11" fill="#4A515C">동시 사용자 (로그)</text>'+
1026
- '<text x="14" y="'+((H-Bm+T)/2)+'" text-anchor="middle" font-size="11" fill="#4A515C" transform="rotate(-90 14 '+((H-Bm+T)/2)+')">GPU당 tok/s (로그)</text>';
1027
 
1028
- /* 물리 상한선 */
1029
  if(cap){
1030
- var off = cap>y1, cy = off ? T+4 : ly(cap);
1031
- s+='<line x1="'+L+'" y1="'+cy+'" x2="'+(W-R)+'" y2="'+cy+'" stroke="#B0432C" stroke-width="1.6" stroke-dasharray="6 4" opacity="'+(off?'.45':'1')+'"/>'+
1032
- '<text x="'+(W-R-4)+'" y="'+(cy+(off?14:-6))+'" text-anchor="end" font-size="10" font-weight="700" fill="#B0432C">물리 상한 '+
1033
- FX(Math.round(cap))+(off?' (축 밖)':'')+'</text>';
1034
  }
1035
- /* 남의 기록 */
1036
- peers.forEach(function(r){
1037
- if(!r.c) return;
1038
- s+='<circle cx="'+lx(r.c).toFixed(1)+'" cy="'+ly(r.pg).toFixed(1)+'" r="'+(r.own?4.4:3.1)+
1039
- '" fill="'+(r.own?'#3963CF':'#B8B0A0')+'" opacity="'+(r.own?'.95':'.55')+'"><title>'+
1040
- (r.m||'')+' · '+(r.by||'')+' · c'+r.c+' · '+FX(r.pg)+' tok/s/GPU · '+(r.gr||'')+'</title></circle>';
1041
  });
1042
- /* 귀사 */
1043
  if(est){
1044
- var px=lx(con), py=ly(est);
1045
- s+='<line x1="'+px+'" y1="'+T+'" x2="'+px+'" y2="'+(H-Bm)+'" stroke="#2A7A4F" stroke-dasharray="3 3" opacity=".5"/>'+
1046
- '<circle cx="'+px+'" cy="'+py+'" r="12" fill="#2A7A4F" opacity=".16"/>'+
1047
  '<circle cx="'+px+'" cy="'+py+'" r="6" fill="#2A7A4F" stroke="#fff" stroke-width="2"/>'+
1048
- '<text x="'+(px+14)+'" y="'+(py-10)+'" font-size="11.5" font-weight="700" fill="#2A7A4F">귀사</text>';
 
1049
  }
1050
  $b('plot').innerHTML=s+'</g>';
1051
  }
 
206
 
207
  .board{display:grid;grid-template-columns:minmax(260px,340px) 1fr;gap:20px;margin-top:30px;align-items:start}
208
  .bpanel .f2{display:grid;grid-template-columns:1fr 1fr;gap:12px}
209
+ .bpanel .f3{display:grid;grid-template-columns:1fr 1fr 1fr;gap:10px}
210
  .bpanel select,.bpanel input{width:100%;padding:10px 12px;border:1px solid var(--line2);
211
  background:var(--surface);color:var(--ink);font:inherit;font-size:14px;border-radius:0}
212
  .bpanel select:focus,.bpanel input:focus{outline:2px solid var(--blue);outline-offset:-2px}
 
616
  <div class="panel bpanel">
617
  <div class="field"><label for="bGpu">쓰시는 GPU</label>
618
  <select id="bGpu"></select></div>
619
+ <div class="f3">
620
  <div class="field"><label for="bQ">양자화</label><select id="bQ"></select></div>
621
+ <div class="field"><label for="bTot">모델 크기 (십억)</label>
622
+ <input id="bTot" type="number" value="35" min="0.1" step="0.1"></div>
623
+ <div class="field"><label for="bAct">그중 활성 (십억)</label>
624
  <input id="bAct" type="number" value="3" min="0.1" step="0.1"></div>
625
  </div>
626
  <div class="f2">
 
665
  <div class="note" id="bNote"><p>GPU를 고르면 계산합니다.</p></div>
666
 
667
  <div class="tw"><table class="brank">
668
+ <thead><tr><th>#</th><th>모델</th><th>잰 곳</th><th class="n">크기</th>
669
  <th class="n">GPU당 tok/s</th><th>등급</th></tr></thead>
670
  <tbody id="bTop"></tbody>
671
  </table></div>
 
882
  $('sendMsg').textContent = '메일 창이 열립니다. 열리지 않으면 contact@quantid.ai 로 보내주십시오.';
883
  });
884
 
885
+ /* ===== 09 · 대조 — 업계 기록 위에 지금 설정 찍기 ===== */
886
  (function(){
887
  var B=null, $b=function(i){return document.getElementById(i)};
888
+ var W=560,H=400, L=58,R=14,T=18,Bm=44;
889
  var FX=function(n){return n.toLocaleString('ko-KR')};
890
 
891
+ fetch('board.json').then(function(r){return r.json()}).then(function(d){B=d; init();})
892
+ .catch(function(){ $b('bNote').innerHTML='<p>대조 자료를 불러오지 못했습니다.</p>'; });
 
 
 
893
 
894
  function init(){
895
  var gs=Object.keys(B.hw).filter(function(k){
896
+ return B.records.some(function(r){return r.g===k}); }).sort();
897
+ $b('bGpu').innerHTML=gs.map(function(k){
 
898
  var n=B.records.filter(function(r){return r.g===k}).length;
899
+ return '<option value="'+k+'">'+k+' ('+n+'건)</option>'; }).join('');
900
+ $b('bGpu').value=gs.slice().sort(function(a,b){
901
+ return B.records.filter(function(r){return r.g===b}).length
902
+ - B.records.filter(function(r){return r.g===a}).length; })[0];
903
 
904
+ $b('bQ').innerHTML=Object.keys(B.bpp).map(function(q){
905
+ return '<option value="'+q+'"'+(q==='FP8'?' selected':'')+'>'+q+'</option>'; }).join('');
 
 
906
 
907
+ var withC=B.records.filter(function(r){return r.c}).length;
908
+ $b('bMeta').textContent='· 기록 '+B.records.length+'건 · GPU '+Object.keys(B.hw).length+'종 · 기준일 '+B.updated;
909
  $b('bSrc').textContent='전력은 제조사가 적어둔 정격이라 실제 쓰는 양보다 넉넉하게 잡힙니다. CPU와 냉각은 안 들어갔습니다. '+
910
  '클라우드 값은 '+(B.src.price_by||[]).join(' · ')+'가 '+B.src.as_of+'에 내건 정가고, 약정하면 이보다 쌉니다. '+
911
+ '동시 사용자 수는 '+B.records.length+'건 가운데 '+withC+'건만 공개돼 있어(MLPerf는 배치 크기를 안 밝힙니다) '+
912
+ '가로축은 모두가 밝힌 모델 크기로 잡았습니다. 둘 다 견적이 아니라 서로 견주는 잣대로 봐주십시오.';
913
 
914
+ ['bGpu','bQ','bTot','bAct','bCon','bGn','bMon','bOut','bKwh'].forEach(function(id){
915
+ var e=$b(id); if(!e) return;
916
+ e.addEventListener('input', calc); e.addEventListener('change', calc); });
 
917
  calc();
918
  }
919
 
920
+ /* 물리 상한 — 디코드 한 스텝은 활성 가중치를 한 번 읽는다 */
 
921
  function ceil(hw, actB, q, con){
922
  if(!hw||!hw.bw||!actB) return null;
923
+ return con*(hw.bw*1e12)/(actB*1e9*(B.bpp[q]||1));
 
924
  }
925
 
926
  function calc(){
927
  var g=$b('bGpu').value; if(!g||!B) return;
928
  var hw=B.hw[g], q=$b('bQ').value,
929
+ tot=+$b('bTot').value||1, act=+$b('bAct').value||1,
930
+ con=+$b('bCon').value||1, mon=+$b('bMon').value||1,
931
+ out=+$b('bOut').value||1, kwh=+$b('bKwh').value||0;
 
932
 
933
  var peers=B.records.filter(function(r){return r.g===g});
934
+ var cap=ceil(hw, act, q, con);
935
 
936
+ /* 비슷한 크기 모델의 실측에 견준다 */
937
+ var near=peers.filter(function(r){return r.tb && r.tb>=tot/2.5 && r.tb<=tot*2.5});
938
+ var basis=near.length>=3?near:peers;
939
+ var est=null;
940
  if(basis.length){
941
  var a=basis.map(function(r){return r.pg}).sort(function(x,y){return x-y});
942
+ est=a[Math.floor(a.length/2)];
943
  }
944
+ var cut=false;
945
+ if(est&&cap&&est>cap){ est=cap; cut=true; }
946
 
947
+ draw(peers, cap, tot, est);
948
 
949
  if(!est){
950
  $b('bNote').innerHTML='<p>이 GPU는 기록이 모자라서 추정하지 않겠습니다. 없는 숫자를 지어내는 것보다 낫습니다.</p>';
 
952
  return;
953
  }
954
 
 
955
  var better=peers.filter(function(r){return r.pg>est}).length;
956
+ $b('kRank').textContent='상위 '+Math.max(1,Math.round((better+1)/peers.length*100))+'%';
 
957
  $b('kRankSub').textContent=g+' 기록 '+peers.length+'건 가운데';
958
 
959
+ var needTok=mon*out, need=Math.max(1, Math.ceil(needTok/(est*3600*24*30.4)));
 
 
 
960
  $b('kGpu').textContent=FX(need)+'장';
961
  $b('kGpuSub').textContent='한 달 '+FX(Math.round(needTok/1e6))+'백만 토큰을 넘기려면';
962
 
963
+ if(hw.w){ var kW=need*hw.w/1000, mWh=kW*730;
 
 
964
  $b('kPow').textContent=FX(Math.round(mWh*kwh))+'원';
965
  $b('kPowSub').textContent=FX(Math.round(mWh))+' kWh · '+kW.toFixed(1)+' kW';
966
  } else { $b('kPow').textContent='—'; $b('kPowSub').textContent='이 GPU는 정격이 공개 안 됨'; }
967
 
968
+ if(hw.usd){ var usd=need*hw.usd*730;
 
 
969
  $b('kUsd').textContent=FX(Math.round(usd*1380/1e4))+'만원';
970
  $b('kUsdSub').textContent='$'+FX(Math.round(usd))+' · 환율 1,380원';
971
  } else { $b('kUsd').textContent='—'; $b('kUsdSub').textContent='공개된 시간당 단가가 없음'; }
972
 
973
+ var others=basis.filter(function(r){return !r.own}).length;
974
  $b('bNote').innerHTML='<p>이 설정이면 <strong>GPU 한 장에 초당 '+FX(Math.round(est))+'토큰</strong>쯤 나옵니다. '+
975
+ tot+'B 안팎 모델을 '+g+'에서 돌린 기록 '+basis.length+'건'+
976
+ (others?'(남이 잰 것 '+others+'건 포함)':'')+'의 한가운데 값입니다'+
977
+ (cut?'. 원래 중앙값은 물리적으로 불가능한 수준이라 상한까지 낮췄습니다':'')+'. '+
978
+ '<b>직접 잰 게 아니라 남의 기록에 견줘 본 값</b>이라 모델이나 서버 설정이 다르면 달라집니다. '+
979
  '도입 검토하실 때 쓰시는 장비에서 직접 재 드립니다.'+
980
  (cap?' 참고로 이 조합이 물리적으로 넘을 수 없는 선은 초당 '+FX(Math.round(cap))+'토큰입니다.':'')+'</p>';
981
 
982
+ $b('bTop').innerHTML=peers.slice().sort(function(a,b){return b.pg-a.pg}).slice(0,8)
983
+ .map(function(r,i){
984
+ return '<tr'+(r.own?' class="mine"':'')+'><td>'+(i+1)+'</td><td>'+(r.m||'—')+
985
+ '</td><td>'+(r.by||'—')+'</td><td class="n">'+(r.tb?r.tb+'B':'—')+
986
+ '</td><td class="n">'+FX(r.pg)+'</td><td><span class="gr g-'+(r.gr||'')+'">'+(r.gr||'—')+'</span></td></tr>';
987
+ }).join('');
988
  }
989
 
990
+ function draw(peers, cap, tot, est){
991
+ var pts=peers.filter(function(r){return r.tb});
992
+ var xs=pts.map(function(r){return r.tb}).concat([tot]),
993
+ ys=pts.map(function(r){return r.pg}).concat(est?[est]:[]);
994
+ if(!xs.length) { $b('plot').innerHTML=''; return; }
995
+ var x0=Math.max(.3,Math.min.apply(null,xs)), x1=Math.max.apply(null,xs),
996
  y0=Math.max(.5,Math.min.apply(null,ys)), y1=Math.max.apply(null,ys);
997
  x1=Math.max(x1,x0*2); y1=Math.max(y1,y0*2);
998
+ var lx=function(v){return L+(Math.log10(Math.min(Math.max(v,x0),x1))-Math.log10(x0))/(Math.log10(x1)-Math.log10(x0))*(W-L-R)},
999
+ ly=function(v){return H-Bm-(Math.log10(Math.min(Math.max(v,y0),y1))-Math.log10(y0))/(Math.log10(y1)-Math.log10(y0))*(H-T-Bm)};
1000
 
1001
+ var s='<g font-family="IBM Plex Sans KR, sans-serif">', p;
1002
+ for(p=Math.ceil(Math.log10(y0)); p<=Math.floor(Math.log10(y1)); p++){
1003
+ var yy=ly(Math.pow(10,p)), lab=Math.pow(10,p);
 
1004
  s+='<line x1="'+L+'" y1="'+yy+'" x2="'+(W-R)+'" y2="'+yy+'" stroke="#DCD6C9" stroke-dasharray="2 3"/>'+
1005
  '<text x="'+(L-7)+'" y="'+(yy+4)+'" text-anchor="end" font-size="9.5" fill="#8A8F98">'+
1006
+ (lab>=1000?(lab/1000)+'k':lab)+'</text>';
1007
  }
1008
+ for(p=Math.ceil(Math.log10(x0)); p<=Math.floor(Math.log10(x1)); p++){
1009
+ var xx=lx(Math.pow(10,p));
1010
+ s+='<text x="'+xx+'" y="'+(H-Bm+16)+'" text-anchor="middle" font-size="9.5" fill="#8A8F98">'+Math.pow(10,p)+'B</text>';
1011
  }
1012
  s+='<line x1="'+L+'" y1="'+(H-Bm)+'" x2="'+(W-R)+'" y2="'+(H-Bm)+'" stroke="#C4BCAB"/>'+
1013
  '<line x1="'+L+'" y1="'+T+'" x2="'+L+'" y2="'+(H-Bm)+'" stroke="#C4BCAB"/>'+
1014
+ '<text x="'+((W+L)/2)+'" y="'+(H-8)+'" text-anchor="middle" font-size="11" fill="#4A515C">모델 크기 · 총 파라미터 (로그)</text>'+
1015
+ '<text x="14" y="'+((H-Bm+T)/2)+'" text-anchor="middle" font-size="11" fill="#4A515C" transform="rotate(-90 14 '+((H-Bm+T)/2)+')">GPU 한 장당 초당 토큰 (로그)</text>';
1016
 
 
1017
  if(cap){
1018
+ var off=cap>y1, cy=off?T+4:ly(cap);
1019
+ s+='<line x1="'+L+'" y1="'+cy+'" x2="'+(W-R)+'" y2="'+cy+'" stroke="#B0432C" stroke-width="1.6" stroke-dasharray="6 4" opacity="'+(off?'.4':'1')+'"/>'+
1020
+ '<text x="'+(W-R-4)+'" y="'+(cy+(off?14:-6))+'" text-anchor="end" font-size="10" font-weight="700" fill="#B0432C">'+
1021
+ '넘을 수 없는 선 '+FX(Math.round(cap))+(off?' (축 밖)':'')+'</text>';
1022
  }
1023
+ pts.forEach(function(r){
1024
+ s+='<circle cx="'+lx(r.tb).toFixed(1)+'" cy="'+ly(r.pg).toFixed(1)+'" r="'+(r.own?4.6:3.2)+
1025
+ '" fill="'+(r.own?'#3963CF':'#B8B0A0')+'" opacity="'+(r.own?'.95':'.5')+'"><title>'+
1026
+ (r.m||'')+' · '+(r.by||'')+' · '+(r.tb||'?')+'B · '+FX(r.pg)+' tok/s/GPU · '+(r.gr||'')+'</title></circle>';
 
 
1027
  });
 
1028
  if(est){
1029
+ var px=lx(tot), py=ly(est);
1030
+ s+='<circle cx="'+px+'" cy="'+py+'" r="13" fill="#2A7A4F" opacity=".15"/>'+
 
1031
  '<circle cx="'+px+'" cy="'+py+'" r="6" fill="#2A7A4F" stroke="#fff" stroke-width="2"/>'+
1032
+ '<text x="'+(px<W-90?px+14:px-14)+'" y="'+(py-11)+'" text-anchor="'+(px<W-90?'start':'end')+
1033
+ '" font-size="11.5" font-weight="700" fill="#2A7A4F">지금 설정</text>';
1034
  }
1035
  $b('plot').innerHTML=s+'</g>';
1036
  }