Spaces:
Running
Running
| import os | |
| import time | |
| import json | |
| import sys | |
| import gradio as gr | |
| # এই ভার্সনটা ইচ্ছাকৃতভাবে CPU-only — এই Space-এ GPU (ZeroGPU) সাথে llama.cpp-এর | |
| # persistent CUDA context ঠিকভাবে মিলছিল না (প্রতিটা রিকোয়েস্টে GPU attach/detach হওয়ায় | |
| # মডেল আবার লোড করতে হতো), তাই এখানে GPU-চেষ্টা ও `spaces` ডিপেন্ডেন্সি বাদ দেওয়া হয়েছে। | |
| # গুরুত্বপূর্ণ: os.cpu_count() কন্টেইনারের হোস্ট মেশিনের মোট কোর সংখ্যা রিপোর্ট করে, | |
| # কিন্তু Spaces-এর মতো শেয়ার্ড হোস্টে আপনার কন্টেইনার আসলে তার চেয়ে অনেক কম CPU quota | |
| # (cgroup affinity) পায়। os.cpu_count() ধরে থ্রেড বানালে বরাদ্দের চেয়ে বেশি থ্রেড | |
| # একে অপরের সাথে প্রতিযোগিতা করে (context-switch overhead), যেটা গতি কমিয়ে দেয়। | |
| # sched_getaffinity আসল বরাদ্দকৃত কোর সংখ্যা দেয় (শুধু Linux-এ কাজ করে, তাই fallback রাখা হলো)। | |
| try: | |
| cpu_count = len(os.sched_getaffinity(0)) | |
| except AttributeError: | |
| cpu_count = os.cpu_count() or 4 | |
| print(f"🧵 ব্যবহারযোগ্য CPU কোর সনাক্ত হয়েছে: {cpu_count}") | |
| # BLAS/OpenMP ব্যাকএন্ড যদি থাকে, সেটাও যেন একই সংখ্যক থ্রেড ব্যবহার করে — | |
| # নাহলে llama.cpp-এর থ্রেড + BLAS-এর নিজস্ব থ্রেড মিলে ওভারসাবস্ক্রাইব হতে পারে। | |
| # native library লোড হওয়ার আগেই সেট করা জরুরি, তাই llama_cpp ইম্পোর্টের আগে বসানো হলো। | |
| os.environ.setdefault("OMP_NUM_THREADS", str(cpu_count)) | |
| os.environ.setdefault("OPENBLAS_NUM_THREADS", str(cpu_count)) | |
| # llama_cpp ইম্পোর্ট | |
| # | |
| # নোট: Bonsai-27B-Q1_0.gguf-এর Q1_0 (1-বিট) ফরম্যাট mainline llama.cpp-তেই সাপোর্টেড — | |
| # CPU ব্যাকএন্ডে কাজ করার জন্য আলাদা কোনো কাস্টম কার্নেল/ফর্ক লাগে না। | |
| try: | |
| from llama_cpp import Llama | |
| print("✅ llama-cpp-python ইম্পোর্ট সফল") | |
| except Exception as e: | |
| print(f"❌ llama-cpp-python ইম্পোর্ট ব্যর্থ: {e}") | |
| sys.exit(1) | |
| # --- ১. মডেল কনফিগারেশন --- | |
| # ================================================================================== | |
| # নিচে যে মডেলটা চালাতে চান, তার দুইটা লাইনের সামনে থেকে '#' সরান। | |
| # আর বাকি সব মডেলের লাইনগুলো '#' দিয়ে কমেন্ট করা রাখুন — একসাথে শুধু ১টা সক্রিয় থাকবে। | |
| # ================================================================================== | |
| # --- Bonsai 27B (1-বিট, ~3.8GB, সবচেয়ে ছোট/দ্রুত) --- | |
| #MODEL_REPO = "prism-ml/Bonsai-27B-gguf" | |
| #MODEL_FILE = "Bonsai-27B-Q1_0.gguf" | |
| # --- Gemma 4 E4B (Q3_K_M, ~4.9GB) --- | |
| MODEL_REPO = "bartowski/google_gemma-4-E4B-it-GGUF" | |
| MODEL_FILE = "google_gemma-4-E4B-it-Q3_K_M.gguf" | |
| # --- এখানে নতুন মডেল যোগ করতে চাইলে এই প্যাটার্নে আরেকটা জোড়া লাইন যোগ করুন: | |
| # MODEL_REPO = "<hugging-face-repo-id>" | |
| # MODEL_FILE = "<gguf-ফাইলের-নাম>" | |
| print("⏳ মডেল ডাউনলোড হচ্ছে...") | |
| from huggingface_hub import hf_hub_download | |
| model_path = hf_hub_download( | |
| repo_id=MODEL_REPO, | |
| filename=MODEL_FILE, | |
| local_dir="./models", | |
| token=None | |
| ) | |
| print(f"✅ ডাউনলোড সম্পূর্ণ: {model_path}") | |
| # --- ২. মডেল লোড --- | |
| print("⏳ মডেল লোড হচ্ছে...") | |
| def load_model(): | |
| """CPU-তে সর্বোচ্চ থ্রুপুটের জন্য টিউন করা লোড""" | |
| print("🐢 CPU মোডে লোড হচ্ছে...") | |
| llm = Llama( | |
| model_path=model_path, | |
| n_ctx=4096, # কনটেক্সট যত বড়, প্রম্পট প্রসেসিং তত ধীর — CPU-তে রক্ষণশীল রাখা ভালো | |
| n_gpu_layers=0, # CPU-only | |
| n_threads=cpu_count, # জেনারেশন থ্রেড — আসল বরাদ্দকৃত কোর সংখ্যা | |
| n_threads_batch=cpu_count, # প্রম্পট-প্রসেসিং (ব্যাচ) থ্রেড | |
| n_batch=256, # CPU-তে ছোট batch সাধারণত বড়টার চেয়ে কার্যকর হয় | |
| type_k=8, # KV cache-এর K অংশ Q8_0-এ কোয়ান্টাইজ — মেমরি-ব্যান্ডউইথ বাঁচায় | |
| # (V cache কোয়ান্টাইজেশনের জন্য flash_attn লাগে, তাই এটা বাদ) | |
| use_mmap=True, # ডিস্ক থেকে সরাসরি ম্যাপ করে, RAM-এ পুরো কপি লাগে না | |
| use_mlock=False, # RAM লক না করা — কম RAM এর হোস্টে সোয়াপ/ক্র্যাশ ঠেকায় | |
| verbose=True | |
| ) | |
| print("✅ CPU মোডে সফলভাবে লোড হয়েছে!") | |
| return llm, "CPU" | |
| try: | |
| llm, device_mode = load_model() | |
| except Exception as e: | |
| print(f"❌ মডেল লোড করা সম্ভব হয়নি: {e}") | |
| sys.exit(1) | |
| # --- ৩. ওয়ার্মআপ --- | |
| try: | |
| print("🔥 ওয়ার্মআপ হচ্ছে...") | |
| llm.create_chat_completion( | |
| messages=[{"role": "user", "content": "হাই"}], | |
| max_tokens=1 | |
| ) | |
| print(f"✅ ওয়ার্মআপ সম্পন্ন! ({device_mode})") | |
| except Exception as e: | |
| print(f"⚠️ ওয়ার্মআপে সমস্যা: {e}") | |
| # ===== ৪. চ্যাট জেনারেশন ফাংশন ===== | |
| def generate_reply(user_prompt): | |
| """মডেল থেকে স্ট্রিমিং টেক্সট জেনারেট করে; পূর্ণ টেক্সট + মেটা রিটার্ন করে (জেনারেটর) | |
| create_chat_completion() ব্যবহার করা হচ্ছে (হার্ডকোড করা প্রম্পট স্ট্রিং না) — এটা | |
| প্রতিটা GGUF ফাইলের ভেতরে embedded chat template নিজে থেকেই ব্যবহার করে, তাই | |
| উপরে MODEL_REPO/MODEL_FILE বদলালে (Bonsai, Gemma, বা অন্য যেকোনো মডেল) প্রম্পট | |
| ফরম্যাট আলাদা করে ঠিক করার দরকার হয় না। | |
| """ | |
| start_time = time.time() | |
| start_str = time.strftime('%H:%M:%S', time.localtime(start_time)) | |
| first_token_time = None | |
| full_text = "" | |
| try: | |
| stream = llm.create_chat_completion( | |
| messages=[{"role": "user", "content": user_prompt}], | |
| max_tokens=256, | |
| temperature=0.1, | |
| top_p=0.2, | |
| top_k=40, | |
| repeat_penalty=1.1, | |
| stream=True | |
| ) | |
| for chunk in stream: | |
| delta = chunk["choices"][0].get("delta", {}) | |
| token_text = delta.get("content") | |
| if token_text: | |
| if first_token_time is None: | |
| first_token_time = time.time() - start_time | |
| full_text += token_text | |
| yield full_text, None | |
| except Exception as e: | |
| print(f"❌ জেনারেশন ত্রুটি: {e}") | |
| full_text += f"\n⚠️ মডেল ত্রুটি: {str(e)}" | |
| yield full_text, None | |
| finally: | |
| end_time = time.time() | |
| end_str = time.strftime('%H:%M:%S', time.localtime(end_time)) | |
| elapsed = round(end_time - start_time, 2) | |
| first_token = round(first_token_time, 2) if first_token_time else 0 | |
| meta = { | |
| "start": start_str, | |
| "end": end_str, | |
| "elapsed": elapsed, | |
| "first_token_time": first_token | |
| } | |
| yield full_text, meta | |
| def chat_fn(user_prompt, history): | |
| """Gradio Chatbot-এর জন্য wrapper: history আপডেট করে stream করে""" | |
| if not user_prompt or not user_prompt.strip(): | |
| yield history, "" | |
| return | |
| history = history + [ | |
| {"role": "user", "content": user_prompt}, | |
| {"role": "assistant", "content": "⏳ উত্তর তৈরি হচ্ছে..."} | |
| ] | |
| yield history, "" | |
| last_meta = None | |
| for partial_text, meta in generate_reply(user_prompt): | |
| history[-1]["content"] = partial_text | |
| if meta is not None: | |
| last_meta = meta | |
| yield history, "" | |
| if last_meta is not None: | |
| time_info = ( | |
| f"⏱️ **প্রথম টোকেন:** {last_meta['first_token_time']}s | " | |
| f"**মোট সময়:** {last_meta['elapsed']}s | " | |
| f"**শুরু:** {last_meta['start']} | " | |
| f"**শেষ:** {last_meta['end']}" | |
| ) | |
| history.append({"role": "assistant", "content": time_info}) | |
| yield history, "" | |
| def clear_chat(): | |
| return [], "" | |
| # ===== ৫. কাস্টম CSS ===== | |
| CUSTOM_CSS = """ | |
| .gradio-container { | |
| max-width: 900px !important; | |
| margin: 20px auto !important; | |
| background: #f0f2f5 !important; | |
| } | |
| #header-box { | |
| background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); | |
| color: white; | |
| padding: 15px 20px; | |
| border-radius: 16px 16px 0 0; | |
| margin-bottom: 0px; | |
| } | |
| #header-box h2 { margin: 0; font-weight: 400; } | |
| #header-box small { opacity: 0.85; font-size: 14px; } | |
| .message-wrap .message.user { | |
| background: #007bff !important; | |
| color: white !important; | |
| border-radius: 18px !important; | |
| } | |
| .message-wrap .message.bot { | |
| background: #e9ecef !important; | |
| color: #333 !important; | |
| border-radius: 18px !important; | |
| } | |
| footer { display: none !important; } | |
| """ | |
| DEVICE_BADGE = "🐢 CPU মোড" | |
| MODEL_LABEL = MODEL_FILE.rsplit(".", 1)[0] # ফাইলের নাম থেকে এক্সটেনশন বাদ দিয়ে ডিসপ্লে-নাম বানানো | |
| with gr.Blocks(title=f"{MODEL_LABEL} চ্যাট (CPU)") as demo: | |
| gr.HTML(f""" | |
| <div id="header-box"> | |
| <h2>🌳 {MODEL_LABEL} চ্যাট</h2> | |
| <small>{MODEL_REPO} • {DEVICE_BADGE} — উত্তর আসতে কিছুটা সময় লাগতে পারে</small> | |
| </div> | |
| """) | |
| chatbot = gr.Chatbot( | |
| value=[{"role": "assistant", "content": f"👋 হ্যালো! আমি {MODEL_LABEL}। আপনি কী জানতে চান?"}], | |
| height=400, | |
| show_label=False | |
| ) | |
| with gr.Row(): | |
| user_input = gr.Textbox( | |
| placeholder="এখানে প্রশ্ন লিখুন...", | |
| show_label=False, | |
| scale=8 | |
| ) | |
| send_btn = gr.Button("পাঠান", scale=1, variant="primary") | |
| clear_btn = gr.Button("🗑️", scale=1) | |
| user_input.submit(chat_fn, [user_input, chatbot], [chatbot, user_input]) | |
| send_btn.click(chat_fn, [user_input, chatbot], [chatbot, user_input]) | |
| clear_btn.click(clear_chat, None, [chatbot, user_input]) | |
| gr.Markdown( | |
| f"<div style='text-align:center;color:#a0aec0;font-size:12px;'>" | |
| f"Powered by llama-cpp-python • {MODEL_REPO} ({MODEL_LABEL})</div>" | |
| ) | |
| if __name__ == "__main__": | |
| demo.queue().launch(server_name="0.0.0.0", server_port=7860, css=CUSTOM_CSS) | |