GPU-Mart / index.html
VickyRDP's picture
Use greedy decoding: do_sample sampling path fails on fp16 logits in transformers.js 3.8.1; remove temp instrumentation
d6c22f2 verified
Raw
History Blame Contribute Delete
12.3 kB
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>GPU Mart Assistant — voice + text AI for RDP GPU Mart</title>
<meta name="description" content="Ask anything about RDP GPU Mart (rdp.in/gpu-mart) — India's datacenter-grade GPU marketplace. A small fine-tuned model, running right in your browser, with voice and text.">
<style>
:root { --red:#d82228; --bg:#e8e8e8; --ink:#1a1a1a; --card:#ffffff; }
* { box-sizing:border-box; margin:0; padding:0; }
body { font-family:'Segoe UI',system-ui,-apple-system,sans-serif; background:var(--bg); color:var(--ink);
display:flex; flex-direction:column; align-items:center; min-height:100vh; }
header { width:100%; background:#fff; border-bottom:3px solid var(--red); padding:14px 20px; text-align:center; }
header h1 { font-size:1.25rem; } header h1 span { color:var(--red); }
header p { font-size:.85rem; color:#555; margin-top:2px; }
#status { width:min(760px,94vw); margin:10px auto 0; font-size:.85rem; color:#444; background:#fff;
border-radius:10px; padding:8px 14px; border-left:4px solid var(--red); }
#bar { height:6px; background:#eee; border-radius:3px; margin-top:6px; overflow:hidden; display:none; }
#bar > div { height:100%; width:0%; background:var(--red); transition:width .3s; }
#chat { width:min(760px,94vw); flex:1; margin:12px auto; background:var(--card); border-radius:14px;
padding:16px; overflow-y:auto; min-height:340px; max-height:56vh; box-shadow:0 1px 4px rgba(0,0,0,.08); }
.msg { margin:8px 0; padding:10px 14px; border-radius:12px; max-width:85%; line-height:1.45; font-size:.95rem;
white-space:pre-wrap; word-wrap:break-word; }
.user { background:var(--red); color:#fff; margin-left:auto; }
.bot { background:#f2f2f2; }
#controls { width:min(760px,94vw); margin:0 auto 8px; display:flex; gap:8px; }
#box { flex:1; padding:12px 14px; border:1px solid #ccc; border-radius:10px; font-size:1rem; outline:none; }
#box:focus { border-color:var(--red); }
button { border:none; border-radius:10px; padding:12px 18px; font-size:1rem; cursor:pointer; }
#send { background:var(--red); color:#fff; }
#mic { background:#fff; color:var(--red); border:2px solid var(--red); }
#mic.listening { background:var(--red); color:#fff; animation:pulse 1.2s infinite; }
@keyframes pulse { 50% { opacity:.6; } }
#opts { width:min(760px,94vw); margin:0 auto 10px; display:flex; align-items:center; gap:14px; font-size:.85rem; color:#444; flex-wrap:wrap; }
.chip { background:#fff; border:1px solid #ddd; border-radius:16px; padding:5px 12px; cursor:pointer; font-size:.8rem; }
.chip:hover { border-color:var(--red); color:var(--red); }
footer { width:100%; text-align:center; font-size:.75rem; color:#777; padding:10px; }
footer a { color:var(--red); text-decoration:none; }
</style>
</head>
<body>
<header>
<h1>🖥️ GPU Mart <span>Assistant</span></h1>
<p>India's datacenter-grade GPU marketplace — ask by typing or speaking. Runs entirely in your browser.</p>
</header>
<div id="status">⏳ Loading the GPU Mart model…<div id="bar"><div></div></div></div>
<div id="chat"></div>
<div id="opts">
<label><input type="checkbox" id="speakToggle" checked> 🔊 Speak replies</label>
<span class="chip" data-q="What is GPU Mart?">What is GPU Mart?</span>
<span class="chip" data-q="Explain CARINA, QUASAR and DRACO">CARINA / QUASAR / DRACO</span>
<span class="chip" data-q="Price of the DRACO 8x H200 server?">DRACO 8× H200 price</span>
<span class="chip" data-q="How do I get a quote?">Get a quote</span>
</div>
<div id="controls">
<input id="box" type="text" placeholder="Ask about GPU Mart…" autocomplete="off">
<button id="mic" title="Speak your question">🎤</button>
<button id="send">Send</button>
</div>
<footer>
Model: <a href="https://huggingface.co/VickyRDP/gpumart-qwen2.5-0.5b-instruct" target="_blank">gpumart fine-tune</a> ·
Store: <a href="https://rdp.in/gpu-mart/" target="_blank">rdp.in/gpu-mart</a> ·
Sales: +91&nbsp;720&nbsp;794&nbsp;8743
</footer>
<script type="module">
const MODEL_ID = 'VickyRDP/gpumart-qwen2.5-0.5b-instruct';
const SYSTEM = "You are the GPU Mart assistant for RDP GPU Mart (rdp.in/gpu-mart), India's datacenter-grade GPU marketplace by RDP Computers India Private Limited. Answer questions about GPU Mart products, series, pricing, policies and support accurately and concisely.";
const chatEl = document.getElementById('chat');
const statusEl = document.getElementById('status');
const barWrap = document.getElementById('bar');
const bar = barWrap.querySelector('div');
const box = document.getElementById('box');
const sendBtn = document.getElementById('send');
const micBtn = document.getElementById('mic');
const speakToggle = document.getElementById('speakToggle');
const history = [];
let generator = null, busy = false;
function addMsg(text, cls) {
const d = document.createElement('div');
d.className = 'msg ' + cls;
d.textContent = text;
chatEl.appendChild(d);
chatEl.scrollTop = chatEl.scrollHeight;
return d;
}
function speak(text) {
if (!speakToggle.checked || !window.speechSynthesis) return;
speechSynnthesis.cancel();
const u = new SpeechSynthesisUtterance(text.slice(0, 800));
u.lang = 'en-IN'; u.rate = 1.02;
const vs = speechSynthesis.getVoices();
u.voice = vs.find(v => v.lang === 'en-IN') || vs.find(v => v.lang.startsWith('en')) || null;
speechSynthesis.speak(u);
}
async function loadModel() {
try {
const { pipeline, TextStreamer } = await import('https://cdn.jsdelivr.net/npm/@huggingface/transformers@3.8.1');
window.__TextStreamer = TextStreamer;
// Refresh cached small config files (config/tokenizer/generation) so config fixes
// reach returning visitors; the big ONNX weights stay cached.
try {
const c = await caches.open('transformers-cache');
for (const req of await c.keys()) {
if (req.url.includes('gpumart-qwen2.5-0.5b-instruct') && /(config\.json|generation_config\.json|tokenizer_config\.json)$/.test(req.url)) {
await c.delete(req);
}
}
} catch (e) {}
// WASM (CPU) inference on purpose: this fp16 export is numerically stable on the
// WASM backend (verified), while WebGPU shader-f16 execution corrupts the output.
// Do NOT switch this to 'webgpu' without re-exporting a WebGPU-safe ONNX first.
const device = 'wasm';
statusEl.firstChild.textContent = '⏳ Downloading the GPU Mart model (~1.2 GB, one-time — cached for future visits)…';
barWrap.style.display = 'block';
const files = {};
generator = await pipeline('text-generation', MODEL_ID, {
device, dtype: 'fp16',
session_options: { graphOptimizationLevel: 'disabled' },
progress_callback: p => {
if (p.status === 'progress' && p.total) {
files[p.file] = [p.loaded, p.total];
let l = 0, t = 0; for (const k in files) { l += files[k][0]; t += files[k][1]; }
bar.style.width = Math.round(100 * l / t) + '%';
}
}
});
// ---- KV dtype adapter: different ONNX Runtime builds resolve this fp16 graph
// to fp16 or fp32 KV-cache inputs. Learn the expected dtypes by try/flip/retry on
// the first run, then convert feeds to match (verified in both directions).
try {
const HasF16 = typeof Float16Array !== 'undefined';
for (const sess of Object.values(generator.model.sessions || {})) {
const orig = sess.run.bind(sess);
let want = null;
sess.run = async (feeds, opts) => {
let Tctor = null;
for (const k in feeds) if (feeds[k] && feeds[k].constructor) { Tctor = feeds[k].constructor; break; }
const conv = (t, ty) => {
if (ty === 'float16') { if (!HasF16) return t; return new Tctor('float16', Float16Array.from(t.data), t.dims); }
return new Tctor('float32', Float32Array.from(t.data), t.dims);
};
const isF = t => t && (t.type === 'float32' || t.type === 'float16');
if (want) {
for (const k in feeds) { const t = feeds[k], w = want[k];
if (isF(t) && w && t.type !== w) feeds[k] = conv(t, w); }
return orig(feeds, opts);
}
try {
const out = await orig(feeds, opts);
want = {}; for (const k in feeds) if (feeds[k] && feeds[k].type) want[k] = feeds[k].type;
return out;
} catch (e) {
if (!/input data type/i.test(String(e && e.message || e)) || !Tctor) throw e;
console.log('[GM] dtype mismatch — flipping float feeds and retrying');
for (const k in feeds) { const t = feeds[k];
if (isF(t)) feeds[k] = conv(t, t.type === 'float32' ? 'float16' : 'float32'); }
const out = await orig(feeds, opts);
want = {}; for (const k in feeds) if (feeds[k] && feeds[k].type) want[k] = feeds[k].type;
return out;
}
};
}
console.log('[GM] kv dtype adapter installed');
} catch (e) { console.warn('[GM] kv dtype adapter install failed', e); }
barWrap.style.display = 'none';
statusEl.firstChild.textContent = '🟢 GPU Mart model loaded — ask away (type or tap the mic). Replies stream in as they are generated.';
addMsg("Namaste! I'm the GPU Mart assistant — trained on RDP GPU Mart's catalog and policies. Ask me about products, CARINA/QUASAR/DRACO series, pricing, quotes, shipping, warranty or support.", 'bot');
} catch (e) {
console.error(e);
statusEl.firstChild.textContent = '🔴 Could not load the model in this browser. Please use the latest Chrome or Edge. Meanwhile, visit rdp.in/gpu-mart or call +91 720 794 8743.';
}
}
async function respond(text) {
if (!text.trim() || busy) return;
box.value = '';
addMsg(text, 'user');
if (!generator) { addMsg('The model is still loading — one moment please…', 'bot'); return; }
busy = true; sendBtn.disabled = true;
const botEl = addMsg('…', 'bot');
try {
const msgs = [{ role: 'system', content: SYSTEM }, ...history.slice(-6), { role: 'user', content: text }];
let out = '';
const streamer = new window.__TextStreamer(generator.tokenizer, {
skip_prompt: true, skip_special_tokens: true,
callback_function: t => { out += t; botEl.textContent = out; chatEl.scrollTop = chatEl.scrollHeight; }
});
const res = await generator(msgs, { max_new_tokens: 160, do_sample: false, repetition_penalty: 1.1, streamer });
const full = (res[0].generated_text.at(-1).content || out).trim();
botEl.textContent = full;
history.push({ role: 'user', content: text }, { role: 'assistant', content: full });
speak(full);
} catch (e) {
console.error(e);
botEl.textContent = 'Sorry, something went wrong generating a reply. Please try again.';
}
busy = false; sendBtn.disabled = false;
}
sendBtn.onclick = () => respond(box.value);
box.addEventListener('keydown', e => { if (e.key === 'Enter') respond(box.value); });
document.querySelectorAll('.chip').forEach(c => c.onclick = () => respond(c.dataset.q));
// ---- Voice input (Web Speech API) ----
const SR = window.SpeechRecognition || window.webkitSpeechRecognition;
if (!SR) { micBtn.disabled = true; micBtn.title = 'Voice input not supported in this browser'; }
let rec = null, listening = false;
micBtn.onclick = () => {
if (!SR) return;
if (listening) { rec && rec.stop(); return; }
rec = new SR();
rec.lang = 'en-IN'; rec.interimResults = true;
let finalTxt = '';
rec.onresult = e => { let t = ''; for (const r of e.results) t += r[0].transcript; box.value = t;
if (e.results[e.results.length - 1].isFinal) finalTxt = t; };
rec.onend = () => { listening = false; micBtn.classList.remove('listening'); micBtn.textContent = '🎤';
if (finalTxt.trim()) respond(finalTxt); };
rec.onerror = () => { listening = false; micBtn.classList.remove('listening'); micBtn.textContent = '🎤'; };
listening = true; micBtn.classList.add('listening'); micBtn.textContent = '🔴';
rec.start();
};
if (window.speechSynthesis) speechSynthesis.getVoices();
loadModel();
</script>
</body>
</html>