Converted QwQ notebooks to .py files
This commit is contained in:
239
sia/__main__.py
239
sia/__main__.py
@@ -1,121 +1,120 @@
|
|||||||
from aiohttp import web
|
from aiohttp import web
|
||||||
import asyncio
|
import asyncio
|
||||||
|
|
||||||
from .auto_approver import AutoApprover
|
from .auto_approver import AutoApprover
|
||||||
from .config import Config
|
from .config import Config
|
||||||
from .iteration_logger import IterationLogger
|
from .iteration_logger import IterationLogger
|
||||||
from .llm_engine.hf_llm_engine import HfLlmEngine
|
from .llm_engine.hf_llm_engine import HfLlmEngine
|
||||||
from .llm_engine.local_llm_engine import LocalLlmEngine
|
from .llm_engine.local_llm_engine import LocalLlmEngine
|
||||||
from .llm_engine.mistral_llm_engine import MistralLlmEngine
|
from .llm_engine.mistral_llm_engine import MistralLlmEngine
|
||||||
from .llm_engine.openai_llm_engine import OpenAILlmEngine
|
from .llm_engine.openai_llm_engine import OpenAILlmEngine
|
||||||
from .llm_engine.qwq_llm_engine import QwQLlmEngine
|
from .llm_engine.qwq_llm_engine import QwQLlmEngine
|
||||||
from .response_parser import ResponseParser
|
from .response_parser import ResponseParser
|
||||||
from .system_metrics import SystemMetrics
|
from .system_metrics import SystemMetrics
|
||||||
from .web.api import Api
|
from .web.api import Api
|
||||||
from .web.static import Static
|
from .web.static import Static
|
||||||
from .web.websockts import Websockets
|
from .web.websockts import Websockets
|
||||||
from .web_agent import WebAgent
|
from .web_agent import WebAgent
|
||||||
from .web_io_buffer import WebIOBuffer
|
from .web_io_buffer import WebIOBuffer
|
||||||
from .working_memory import WorkingMemory
|
from .working_memory import WorkingMemory
|
||||||
from .xml_validator import XMLValidator
|
from .xml_validator import XMLValidator
|
||||||
|
|
||||||
class Main:
|
class Main:
|
||||||
@classmethod
|
@classmethod
|
||||||
async def create(cls, config: Config):
|
async def create(cls, config: Config):
|
||||||
self = cls()
|
self = cls()
|
||||||
self._config = config
|
self._config = config
|
||||||
|
|
||||||
self._system_prompt = self._config.system_prompt.read_text()
|
self._system_prompt = self._config.system_prompt.read_text()
|
||||||
self._action_schema = self._config.action_schema.read_text()
|
self._action_schema = self._config.action_schema.read_text()
|
||||||
|
|
||||||
# Initialize LLM engines based on config
|
# Initialize LLM engines based on config
|
||||||
self._llms = {}
|
self._llms = {}
|
||||||
|
|
||||||
if config.local_enabled:
|
if config.local_enabled:
|
||||||
self._llms['local'] = LocalLlmEngine(
|
self._llms['local'] = LocalLlmEngine(
|
||||||
config.local_model,
|
config.local_model,
|
||||||
config.local_temperature,
|
config.local_temperature,
|
||||||
config.local_token_limit,
|
config.local_token_limit,
|
||||||
config.local_api_key,
|
config.local_api_key,
|
||||||
)
|
)
|
||||||
|
|
||||||
if config.openai_enabled:
|
if config.openai_enabled:
|
||||||
self._llms['openai'] = OpenAILlmEngine(
|
self._llms['openai'] = OpenAILlmEngine(
|
||||||
config.openai_model,
|
config.openai_model,
|
||||||
config.openai_temperature,
|
config.openai_temperature,
|
||||||
config.openai_token_limit,
|
config.openai_token_limit,
|
||||||
config.openai_api_key,
|
config.openai_api_key,
|
||||||
)
|
)
|
||||||
|
|
||||||
if config.hf_enabled:
|
if config.hf_enabled:
|
||||||
self._llms['hf'] = HfLlmEngine(
|
self._llms['hf'] = HfLlmEngine(
|
||||||
config.hf_model,
|
config.hf_model,
|
||||||
config.hf_temperature,
|
config.hf_temperature,
|
||||||
config.hf_api_key,
|
config.hf_api_key,
|
||||||
)
|
)
|
||||||
|
|
||||||
if config.mistral_enabled:
|
if config.mistral_enabled:
|
||||||
self._llms['mistral'] = MistralLlmEngine(
|
self._llms['mistral'] = MistralLlmEngine(
|
||||||
config.mistral_model,
|
config.mistral_model,
|
||||||
config.mistral_temperature,
|
config.mistral_temperature,
|
||||||
config.mistral_token_limit,
|
config.mistral_token_limit,
|
||||||
config.mistral_api_key,
|
config.mistral_api_key,
|
||||||
)
|
)
|
||||||
|
|
||||||
if config.qwq_enabled:
|
if config.qwq_enabled:
|
||||||
self._llms['qwq'] = QwQLlmEngine(
|
self._llms['qwq'] = QwQLlmEngine(
|
||||||
config.qwq_model,
|
config.qwq_model,
|
||||||
config.qwq_temperature,
|
config.qwq_temperature,
|
||||||
config.qwq_token_limit,
|
config.qwq_token_limit,
|
||||||
config.hf_api_key,
|
)
|
||||||
)
|
|
||||||
|
if not self._llms:
|
||||||
if not self._llms:
|
raise ValueError("No LLM engines enabled in configuration")
|
||||||
raise ValueError("No LLM engines enabled in configuration")
|
|
||||||
|
self._io_buffer = WebIOBuffer()
|
||||||
self._io_buffer = WebIOBuffer()
|
self._working_memory = WorkingMemory()
|
||||||
self._working_memory = WorkingMemory()
|
self._agent = WebAgent(
|
||||||
self._agent = WebAgent(
|
system_prompt=self._system_prompt,
|
||||||
system_prompt=self._system_prompt,
|
action_schema=self._action_schema,
|
||||||
action_schema=self._action_schema,
|
working_memory=self._working_memory,
|
||||||
working_memory=self._working_memory,
|
metrics=SystemMetrics(),
|
||||||
metrics=SystemMetrics(),
|
llms=self._llms,
|
||||||
llms=self._llms,
|
validator=XMLValidator(self._action_schema),
|
||||||
validator=XMLValidator(self._action_schema),
|
parser=ResponseParser(config.work_dir, self._io_buffer),
|
||||||
parser=ResponseParser(config.work_dir, self._io_buffer),
|
iteration_logger=IterationLogger(self._config.iterations_dir, self._system_prompt, self._action_schema),
|
||||||
iteration_logger=IterationLogger(self._config.iterations_dir, self._system_prompt, self._action_schema),
|
)
|
||||||
)
|
self._auto_approver = AutoApprover(self._agent)
|
||||||
self._auto_approver = AutoApprover(self._agent)
|
|
||||||
|
self._app = web.Application()
|
||||||
self._app = web.Application()
|
self._api = Api(config.work_dir, self._app, self._agent, self._io_buffer, self._working_memory, self._auto_approver)
|
||||||
self._api = Api(config.work_dir, self._app, self._agent, self._io_buffer, self._working_memory, self._auto_approver)
|
self._websockets = Websockets(self._app, self._agent, self._io_buffer, self._auto_approver, self._working_memory)
|
||||||
self._websockets = Websockets(self._app, self._agent, self._io_buffer, self._auto_approver, self._working_memory)
|
self._static = Static(self._app, self._config)
|
||||||
self._static = Static(self._app, self._config)
|
|
||||||
|
return self
|
||||||
return self
|
|
||||||
|
@property
|
||||||
@property
|
def app(self):
|
||||||
def app(self):
|
return self._app
|
||||||
return self._app
|
|
||||||
|
async def _serve_index(self, request: web.Request) -> web.Response:
|
||||||
async def _serve_index(self, request: web.Request) -> web.Response:
|
"""Serve the React application HTML for any unmatched routes."""
|
||||||
"""Serve the React application HTML for any unmatched routes."""
|
index_path = self._config.static_files / "index.html"
|
||||||
index_path = self._config.static_files / "index.html"
|
if not index_path.exists():
|
||||||
if not index_path.exists():
|
raise web.HTTPNotFound()
|
||||||
raise web.HTTPNotFound()
|
|
||||||
|
with open(index_path, "r") as f:
|
||||||
with open(index_path, "r") as f:
|
html_content = f.read()
|
||||||
html_content = f.read()
|
|
||||||
|
return web.Response(
|
||||||
return web.Response(
|
text=html_content,
|
||||||
text=html_content,
|
content_type="text/html"
|
||||||
content_type="text/html"
|
)
|
||||||
)
|
|
||||||
|
def main():
|
||||||
def main():
|
loop = asyncio.new_event_loop()
|
||||||
loop = asyncio.new_event_loop()
|
config = Config()
|
||||||
config = Config()
|
main_instance = loop.run_until_complete(Main.create(config))
|
||||||
main_instance = loop.run_until_complete(Main.create(config))
|
print(f"Web server started at http://localhost:{config.port}")
|
||||||
print(f"Web server started at http://localhost:{config.port}")
|
web.run_app(main_instance.app, loop=loop, host=config.host, port=config.port)
|
||||||
web.run_app(main_instance.app, loop=loop, host=config.host, port=config.port)
|
|
||||||
return 0
|
return 0
|
||||||
@@ -1,121 +1,121 @@
|
|||||||
from threading import Thread
|
from threading import Thread
|
||||||
from typing import Iterator, Optional, Callable
|
from typing import Iterator, Optional, Callable
|
||||||
|
|
||||||
from transformers import AutoTokenizer, AutoModelForCausalLM, pipeline, TextIteratorStreamer
|
from transformers import AutoTokenizer, AutoModelForCausalLM, pipeline, TextIteratorStreamer
|
||||||
import torch
|
import torch
|
||||||
|
|
||||||
from . import LlmEngine
|
from . import LlmEngine
|
||||||
from .. import util
|
from .. import util
|
||||||
|
|
||||||
class LocalLlmEngine(LlmEngine):
|
class LocalLlmEngine(LlmEngine):
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
model_path: str,
|
model_path: str,
|
||||||
temperature: float,
|
temperature: float,
|
||||||
token_limit: int,
|
token_limit: int,
|
||||||
api_token: Optional[str],
|
api_token: Optional[str],
|
||||||
):
|
):
|
||||||
"""
|
"""
|
||||||
Initialize the LLM Engine with a model path.
|
Initialize the LLM Engine with a model path.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
model_path: Path to the model weights to be used.
|
model_path: Path to the model weights to be used.
|
||||||
temperature: Temperature for sampling
|
temperature: Temperature for sampling
|
||||||
api_token: Huggingface API key
|
token_limit: Maximum number of tokens to generate
|
||||||
token_limit: Maximum number of tokens to generate
|
api_token: Huggingface API key
|
||||||
"""
|
"""
|
||||||
self._temperature = temperature
|
self._temperature = temperature
|
||||||
self._token_limit = token_limit
|
self._token_limit = token_limit
|
||||||
self._tokenizer = AutoTokenizer.from_pretrained(model_path, token=api_token)
|
self._tokenizer = AutoTokenizer.from_pretrained(model_path, token=api_token)
|
||||||
model = AutoModelForCausalLM.from_pretrained(
|
model = AutoModelForCausalLM.from_pretrained(
|
||||||
model_path,
|
model_path,
|
||||||
return_dict=True,
|
return_dict=True,
|
||||||
low_cpu_mem_usage=True,
|
low_cpu_mem_usage=True,
|
||||||
torch_dtype=torch.bfloat16,
|
torch_dtype=torch.bfloat16,
|
||||||
device_map="auto",
|
device_map="auto",
|
||||||
trust_remote_code=True,
|
trust_remote_code=True,
|
||||||
token=api_token,
|
token=api_token,
|
||||||
)
|
)
|
||||||
if self._tokenizer.pad_token_id is None:
|
if self._tokenizer.pad_token_id is None:
|
||||||
self._tokenizer.pad_token_id = self._tokenizer.eos_token_id
|
self._tokenizer.pad_token_id = self._tokenizer.eos_token_id
|
||||||
if model.config.pad_token_id is None:
|
if model.config.pad_token_id is None:
|
||||||
model.config.pad_token_id = model.config.eos_token_id
|
model.config.pad_token_id = model.config.eos_token_id
|
||||||
self._pipeline = pipeline(
|
self._pipeline = pipeline(
|
||||||
"text-generation",
|
"text-generation",
|
||||||
model=model,
|
model=model,
|
||||||
tokenizer=self._tokenizer,
|
tokenizer=self._tokenizer,
|
||||||
torch_dtype=torch.bfloat16,
|
torch_dtype=torch.bfloat16,
|
||||||
device_map="auto",
|
device_map="auto",
|
||||||
return_full_text=False,
|
return_full_text=False,
|
||||||
)
|
)
|
||||||
|
|
||||||
def infer(self, system_prompt: str, main_context: str, should_stop: Callable[[], bool] = lambda: False) -> Iterator[str]:
|
def infer(self, system_prompt: str, main_context: str, should_stop: Callable[[], bool] = lambda: False) -> Iterator[str]:
|
||||||
"""
|
"""
|
||||||
Run inference using the system prompt and main context.
|
Run inference using the system prompt and main context.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
system_prompt: The system prompt string
|
system_prompt: The system prompt string
|
||||||
main_context: The main context string after templating
|
main_context: The main context string after templating
|
||||||
should_stop: Callback that returns True when inference should stop
|
should_stop: Callback that returns True when inference should stop
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
Iterator[str]: An iterator that yields the generated text.
|
Iterator[str]: An iterator that yields the generated text.
|
||||||
"""
|
"""
|
||||||
messages = [
|
messages = [
|
||||||
{"role": "system", "content": system_prompt},
|
{"role": "system", "content": system_prompt},
|
||||||
{"role": "user", "content": main_context}
|
{"role": "user", "content": main_context}
|
||||||
]
|
]
|
||||||
prompt = self._tokenizer.apply_chat_template(
|
prompt = self._tokenizer.apply_chat_template(
|
||||||
messages, tokenize=False, add_generation_prompt=True
|
messages, tokenize=False, add_generation_prompt=True
|
||||||
)
|
)
|
||||||
streamer = TextIteratorStreamer(
|
streamer = TextIteratorStreamer(
|
||||||
self._tokenizer,
|
self._tokenizer,
|
||||||
skip_prompt=True
|
skip_prompt=True
|
||||||
)
|
)
|
||||||
generation_thread = Thread(target=self._pipeline, kwargs=dict(
|
generation_thread = Thread(target=self._pipeline, kwargs=dict(
|
||||||
text_inputs=prompt,
|
text_inputs=prompt,
|
||||||
do_sample=True,
|
do_sample=True,
|
||||||
temperature=self._temperature,
|
temperature=self._temperature,
|
||||||
max_new_tokens=self.token_limit(),
|
max_new_tokens=self.token_limit(),
|
||||||
streamer=streamer
|
streamer=streamer
|
||||||
))
|
))
|
||||||
generation_thread.start()
|
generation_thread.start()
|
||||||
|
|
||||||
for text in util.stop_before_value(streamer, '<|eot_id|>'):
|
for text in util.stop_before_value(streamer, '<|eot_id|>'):
|
||||||
yield text
|
yield text
|
||||||
if should_stop():
|
if should_stop():
|
||||||
break
|
break
|
||||||
|
|
||||||
generation_thread.join()
|
generation_thread.join()
|
||||||
|
|
||||||
def token_count(self, system_prompt: str, main_context: str) -> int:
|
def token_count(self, system_prompt: str, main_context: str) -> int:
|
||||||
"""
|
"""
|
||||||
Count tokens for the given system prompt and main context.
|
Count tokens for the given system prompt and main context.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
system_prompt: The system prompt string
|
system_prompt: The system prompt string
|
||||||
main_context: The main context string
|
main_context: The main context string
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
int: Total number of tokens
|
int: Total number of tokens
|
||||||
"""
|
"""
|
||||||
messages = [
|
messages = [
|
||||||
{"role": "system", "content": system_prompt},
|
{"role": "system", "content": system_prompt},
|
||||||
{"role": "user", "content": main_context}
|
{"role": "user", "content": main_context}
|
||||||
]
|
]
|
||||||
prompt = self._tokenizer.apply_chat_template(
|
prompt = self._tokenizer.apply_chat_template(
|
||||||
messages, tokenize=False, add_generation_prompt=True
|
messages, tokenize=False, add_generation_prompt=True
|
||||||
)
|
)
|
||||||
return len(self._tokenizer.encode(prompt))
|
return len(self._tokenizer.encode(prompt))
|
||||||
|
|
||||||
def token_limit(self) -> int:
|
def token_limit(self) -> int:
|
||||||
"""
|
"""
|
||||||
Get the model's context window size.
|
Get the model's context window size.
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
int: Maximum number of tokens the model can process
|
int: Maximum number of tokens the model can process
|
||||||
"""
|
"""
|
||||||
if self._token_limit is not None:
|
if self._token_limit is not None:
|
||||||
return self._token_limit
|
return self._token_limit
|
||||||
else:
|
else:
|
||||||
return self._pipeline.model.config.max_position_embeddings
|
return self._pipeline.model.config.max_position_embeddings
|
||||||
|
|||||||
@@ -1,303 +1,124 @@
|
|||||||
from typing import Callable, Iterator, Optional
|
from pathlib import Path
|
||||||
import torch
|
from threading import Thread
|
||||||
from transformers import AutoTokenizer, AutoModelForCausalLM, TextIteratorStreamer
|
from transformers import AutoTokenizer, AutoModelForCausalLM, pipeline, TextIteratorStreamer, BitsAndBytesConfig
|
||||||
from threading import Thread
|
from typing import Callable, Iterator
|
||||||
from pathlib import Path
|
import torch
|
||||||
import sys
|
|
||||||
import gc
|
from . import LlmEngine
|
||||||
import os
|
from .. import util
|
||||||
import re
|
|
||||||
|
class QwQLlmEngine(LlmEngine):
|
||||||
from . import LlmEngine
|
|
||||||
from .. import util
|
def __init__(
|
||||||
|
self,
|
||||||
class QwQLlmEngine(LlmEngine):
|
model_path: Path,
|
||||||
"""
|
temperature: float,
|
||||||
LLM Engine implementation for QwQ models.
|
token_limit: int = None,
|
||||||
|
):
|
||||||
QwQ is a reasoning-based model with <think> capabilities. This engine handles:
|
"""
|
||||||
1. Proper initialization with recommended parameters
|
Initialize the QwQ LLM Engine.
|
||||||
2. Processing outputs to extract reasoning and actions
|
|
||||||
3. Converting QwQ's format to SIA-compatible action schemas
|
Args:
|
||||||
"""
|
model_path: Local path to the model
|
||||||
|
temperature: Sampling temperature
|
||||||
def __init__(
|
token_limit: Maximum tokens to generate
|
||||||
self,
|
"""
|
||||||
model_path: str,
|
self._temperature = temperature
|
||||||
temperature: float = 0.6, # QwQ recommended default
|
self._token_limit = token_limit
|
||||||
token_limit: Optional[int] = None,
|
|
||||||
api_key: Optional[str] = None,
|
quantization_config = BitsAndBytesConfig(
|
||||||
):
|
load_in_4bit=True,
|
||||||
"""
|
bnb_4bit_compute_dtype=torch.bfloat16,
|
||||||
Initialize the QwQ LLM Engine.
|
bnb_4bit_quant_type="nf4",
|
||||||
|
bnb_4bit_use_double_quant=True
|
||||||
Args:
|
)
|
||||||
model_path: Local path to the model or HF model ID
|
|
||||||
temperature: Sampling temperature (0.6 default as recommended for QwQ)
|
model = AutoModelForCausalLM.from_pretrained(
|
||||||
token_limit: Maximum tokens to generate or context length override
|
model_path,
|
||||||
api_key: HuggingFace API token if needed
|
return_dict=True,
|
||||||
"""
|
device_map="auto",
|
||||||
self._model_path = Path(model_path) if os.path.exists(model_path) else model_path
|
attn_implementation="flash_attention_2",
|
||||||
self._temperature = temperature
|
use_cache=True,
|
||||||
self._token_limit = token_limit
|
quantization_config=quantization_config,
|
||||||
|
)
|
||||||
# QwQ-specific parameters
|
|
||||||
self._top_p = 0.95 # QwQ recommended
|
self._tokenizer = AutoTokenizer.from_pretrained(model_path)
|
||||||
self._min_p = 0.0 # QwQ recommended
|
|
||||||
self._top_k = 40 # QwQ recommended
|
self._pipline = pipeline(
|
||||||
|
"text-generation",
|
||||||
try:
|
model=model,
|
||||||
# Free memory before loading
|
tokenizer=self._tokenizer,
|
||||||
gc.collect()
|
return_full_text=False,
|
||||||
|
)
|
||||||
print(f"Loading QwQ tokenizer from {self._model_path}...")
|
|
||||||
self._tokenizer = AutoTokenizer.from_pretrained(
|
|
||||||
self._model_path,
|
def infer(self, system_prompt: str, main_context: str, should_stop: Callable[[], bool] = lambda: False) -> Iterator[str]:
|
||||||
token=api_key,
|
"""
|
||||||
trust_remote_code=True,
|
Run inference using the system prompt and main context.
|
||||||
)
|
|
||||||
|
Args:
|
||||||
# Set padding token to avoid warnings
|
system_prompt: The system prompt string
|
||||||
if self._tokenizer.pad_token is None:
|
main_context: The main context string after templating
|
||||||
self._tokenizer.pad_token = self._tokenizer.eos_token
|
should_stop: Callback that returns True when inference should stop
|
||||||
|
|
||||||
# Device configuration
|
Returns:
|
||||||
if torch.cuda.is_available():
|
Iterator[str]: An iterator that yields the generated text.
|
||||||
print(f"Loading QwQ model on GPU...")
|
"""
|
||||||
device_map = "auto"
|
messages = [
|
||||||
dtype = torch.bfloat16
|
{"role": "system", "content": system_prompt},
|
||||||
else:
|
{"role": "user", "content": main_context}
|
||||||
print(f"Loading QwQ model on CPU...")
|
]
|
||||||
device_map = "cpu"
|
|
||||||
dtype = torch.float32
|
text = self._tokenizer.apply_chat_template(
|
||||||
|
messages,
|
||||||
# Load model with appropriate settings
|
tokenize=False,
|
||||||
self._model = AutoModelForCausalLM.from_pretrained(
|
add_generation_prompt=True,
|
||||||
self._model_path,
|
)
|
||||||
device_map=device_map,
|
|
||||||
torch_dtype=dtype,
|
streamer = TextIteratorStreamer(
|
||||||
trust_remote_code=True,
|
self._tokenizer,
|
||||||
return_dict=True,
|
skip_prompt=True,
|
||||||
token=api_key,
|
)
|
||||||
)
|
|
||||||
|
generation_thread = Thread(
|
||||||
# Ensure model is in evaluation mode
|
target=self._pipline,
|
||||||
self._model.eval()
|
kwargs=dict(
|
||||||
print("QwQ model loaded successfully.")
|
text_inputs=text,
|
||||||
|
do_sample=True,
|
||||||
# Clear cache after loading
|
temperature=self._temperature,
|
||||||
gc.collect()
|
max_new_tokens=self._token_limit,
|
||||||
|
streamer=streamer,
|
||||||
except Exception as e:
|
)
|
||||||
print(f"Failed to initialize QwQ model: {e}")
|
)
|
||||||
import traceback
|
|
||||||
traceback.print_exc()
|
generation_thread.start()
|
||||||
raise RuntimeError(f"Failed to initialize QwQ model: {e}")
|
|
||||||
|
for text in util.stop_before_value(streamer, '<|eot_id|>'):
|
||||||
def infer(self, system_prompt: str, main_context: str, should_stop: Callable[[], bool] = lambda: False) -> Iterator[str]:
|
yield text
|
||||||
"""
|
if should_stop():
|
||||||
Run inference using the system prompt and main context.
|
break
|
||||||
|
|
||||||
Args:
|
generation_thread.join()
|
||||||
system_prompt: The system prompt string
|
|
||||||
main_context: The main context string after templating
|
def token_count(self, system_prompt: str, main_context: str) -> int:
|
||||||
should_stop: Callback that returns True when inference should stop
|
"""
|
||||||
|
Count tokens for the given system prompt and main context.
|
||||||
Returns:
|
|
||||||
Iterator[str]: An iterator that yields the generated text.
|
Args:
|
||||||
"""
|
system_prompt: The system prompt string
|
||||||
try:
|
main_context: The main context string
|
||||||
# Format as messages for chat template
|
|
||||||
messages = [
|
Returns:
|
||||||
{"role": "system", "content": system_prompt},
|
int: Total number of tokens
|
||||||
{"role": "user", "content": main_context}
|
"""
|
||||||
]
|
messages = [
|
||||||
|
{"role": "system", "content": system_prompt},
|
||||||
# Apply chat template - DO NOT add <think> token as it will be handled by the model
|
{"role": "user", "content": main_context}
|
||||||
text = self._tokenizer.apply_chat_template(
|
]
|
||||||
messages,
|
prompt = self._tokenizer.apply_chat_template(
|
||||||
tokenize=False,
|
messages, tokenize=False, add_generation_prompt=True
|
||||||
add_generation_prompt=True,
|
)
|
||||||
)
|
return len(self._tokenizer.encode(prompt))
|
||||||
|
|
||||||
# Tokenize input
|
def token_limit(self) -> int:
|
||||||
print("Tokenizing input...")
|
return self._token_limit
|
||||||
inputs = self._tokenizer(text, return_tensors="pt").to(self._model.device)
|
|
||||||
|
|
||||||
# Create streamer for token-by-token generation
|
|
||||||
print("Starting generation...")
|
|
||||||
streamer = TextIteratorStreamer(
|
|
||||||
self._tokenizer,
|
|
||||||
skip_prompt=True,
|
|
||||||
skip_special_tokens=True,
|
|
||||||
timeout=60.0
|
|
||||||
)
|
|
||||||
|
|
||||||
# Configure generation with QwQ's recommended parameters
|
|
||||||
generation_kwargs = {
|
|
||||||
"input_ids": inputs.input_ids,
|
|
||||||
"attention_mask": inputs.attention_mask,
|
|
||||||
"max_new_tokens": self.token_limit(),
|
|
||||||
"temperature": self._temperature,
|
|
||||||
"top_p": self._top_p,
|
|
||||||
"top_k": self._top_k,
|
|
||||||
"min_p": self._min_p,
|
|
||||||
"do_sample": True,
|
|
||||||
"streamer": streamer,
|
|
||||||
"repetition_penalty": 1.1,
|
|
||||||
"pad_token_id": self._tokenizer.pad_token_id,
|
|
||||||
"use_cache": True,
|
|
||||||
}
|
|
||||||
|
|
||||||
print("Starting generation thread...")
|
|
||||||
generation_thread = Thread(target=self._model.generate, kwargs=generation_kwargs)
|
|
||||||
generation_thread.start()
|
|
||||||
|
|
||||||
# Accumulate raw output and track think mode
|
|
||||||
raw_output = ""
|
|
||||||
action_extracted = False
|
|
||||||
|
|
||||||
# Process thinking and extract actions
|
|
||||||
try:
|
|
||||||
for text in streamer:
|
|
||||||
raw_output += text
|
|
||||||
|
|
||||||
# Check if we should stop
|
|
||||||
if should_stop():
|
|
||||||
print("Generation stopped by caller")
|
|
||||||
break
|
|
||||||
|
|
||||||
# Extract action if available
|
|
||||||
action = self._extract_action(raw_output)
|
|
||||||
if action and not action_extracted:
|
|
||||||
# We've found an action tag - yield it
|
|
||||||
action_extracted = True
|
|
||||||
yield action
|
|
||||||
elif not action_extracted:
|
|
||||||
# Still in thinking phase or no action yet - yield tokens
|
|
||||||
yield text
|
|
||||||
|
|
||||||
# Process remaining output
|
|
||||||
if raw_output and not action_extracted:
|
|
||||||
final_action = self._process_final_output(raw_output)
|
|
||||||
if final_action:
|
|
||||||
yield final_action
|
|
||||||
|
|
||||||
finally:
|
|
||||||
# Ensure thread is properly joined even if iteration is interrupted
|
|
||||||
generation_thread.join()
|
|
||||||
# Force garbage collection after generation
|
|
||||||
gc.collect()
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
print(f"QwQ inference error: {e}")
|
|
||||||
import traceback
|
|
||||||
traceback.print_exc()
|
|
||||||
# Re-raise to make the failure visible
|
|
||||||
raise RuntimeError(f"QwQ inference failed: {e}")
|
|
||||||
|
|
||||||
def _extract_action(self, text: str) -> Optional[str]:
|
|
||||||
"""
|
|
||||||
Extract SIA-compatible action from QwQ output.
|
|
||||||
Returns the action if found, None if still in thinking mode.
|
|
||||||
"""
|
|
||||||
# Check if we have a complete think block followed by an action
|
|
||||||
think_pattern = r'<think>(.*?)</think>\s*(<\w+.*?>)'
|
|
||||||
match = re.search(think_pattern, text, re.DOTALL)
|
|
||||||
|
|
||||||
if match:
|
|
||||||
# Found a think block followed by an action tag
|
|
||||||
action_start = match.group(2)
|
|
||||||
# Return the action part
|
|
||||||
action_idx = text.index(action_start)
|
|
||||||
return text[action_idx:]
|
|
||||||
|
|
||||||
# Check for direct action (no thinking)
|
|
||||||
action_pattern = r'^(<(?:single|repeat|delete|stop|reasoning|read_stdin|write_stdout).*?>)'
|
|
||||||
match = re.search(action_pattern, text)
|
|
||||||
if match:
|
|
||||||
return text
|
|
||||||
|
|
||||||
return None
|
|
||||||
|
|
||||||
def _process_final_output(self, text: str) -> str:
|
|
||||||
"""
|
|
||||||
Process final output if no action was extracted.
|
|
||||||
Converts thinking content to reasoning if needed.
|
|
||||||
"""
|
|
||||||
# Check if there's thinking content
|
|
||||||
think_pattern = r'<think>(.*?)</think>'
|
|
||||||
match = re.search(think_pattern, text, re.DOTALL)
|
|
||||||
|
|
||||||
if match:
|
|
||||||
# Extract thinking content
|
|
||||||
thinking = match.group(1).strip()
|
|
||||||
if thinking:
|
|
||||||
# Convert to reasoning
|
|
||||||
return f"<reasoning>\n{thinking}\n</reasoning>"
|
|
||||||
|
|
||||||
# If the response has no XML tags but isn't empty, make it reasoning
|
|
||||||
if text.strip() and not re.search(r'<\w+.*?>', text):
|
|
||||||
return f"<reasoning>\n{text.strip()}\n</reasoning>"
|
|
||||||
|
|
||||||
# Return as-is if it already has valid XML tags
|
|
||||||
return text
|
|
||||||
|
|
||||||
def token_count(self, system_prompt: str, main_context: str) -> int:
|
|
||||||
"""
|
|
||||||
Count tokens for the given system prompt and main context.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
system_prompt: The system prompt string
|
|
||||||
main_context: The main context string
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
int: Total number of tokens
|
|
||||||
"""
|
|
||||||
messages = [
|
|
||||||
{"role": "system", "content": system_prompt},
|
|
||||||
{"role": "user", "content": main_context}
|
|
||||||
]
|
|
||||||
text = self._tokenizer.apply_chat_template(
|
|
||||||
messages,
|
|
||||||
tokenize=False,
|
|
||||||
add_generation_prompt=True,
|
|
||||||
)
|
|
||||||
return len(self._tokenizer.encode(text))
|
|
||||||
|
|
||||||
def token_limit(self) -> int:
|
|
||||||
"""
|
|
||||||
Get the model's context window size.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
int: Maximum number of tokens the model can process
|
|
||||||
"""
|
|
||||||
if self._token_limit is not None:
|
|
||||||
return self._token_limit
|
|
||||||
|
|
||||||
# Try to detect model size from config
|
|
||||||
try:
|
|
||||||
if isinstance(self._model_path, Path):
|
|
||||||
config_file = self._model_path / "config.json"
|
|
||||||
if config_file.exists():
|
|
||||||
import json
|
|
||||||
with open(config_file, 'r') as f:
|
|
||||||
config = json.load(f)
|
|
||||||
else:
|
|
||||||
config = self._model.config.to_dict()
|
|
||||||
else:
|
|
||||||
config = self._model.config.to_dict()
|
|
||||||
|
|
||||||
# Check for context length in different possible fields
|
|
||||||
if 'max_position_embeddings' in config:
|
|
||||||
return config['max_position_embeddings']
|
|
||||||
if 'model_max_length' in config:
|
|
||||||
return config['model_max_length']
|
|
||||||
|
|
||||||
# Safe fallback for QwQ - it supports up to 8192 by default
|
|
||||||
return 8192
|
|
||||||
except Exception as e:
|
|
||||||
print(f"Warning: Failed to read model config: {e}")
|
|
||||||
|
|
||||||
# Default fallback
|
|
||||||
return 4096
|
|
||||||
@@ -6,7 +6,6 @@ Fine-tuning for QwQ model
|
|||||||
# Unsloth should be imported before transformers to ensure all optimizations are applied.
|
# Unsloth should be imported before transformers to ensure all optimizations are applied.
|
||||||
from unsloth import FastLanguageModel, is_bfloat16_supported
|
from unsloth import FastLanguageModel, is_bfloat16_supported
|
||||||
|
|
||||||
from .dataset import Dataset
|
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from transformers import TrainingArguments
|
from transformers import TrainingArguments
|
||||||
@@ -15,6 +14,8 @@ from typing import Optional, List
|
|||||||
import argparse
|
import argparse
|
||||||
import os
|
import os
|
||||||
|
|
||||||
|
from .dataset import Dataset
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class Args:
|
class Args:
|
||||||
def __init__(self, args: Optional[List[str]]):
|
def __init__(self, args: Optional[List[str]]):
|
||||||
@@ -78,7 +79,7 @@ def main():
|
|||||||
load_in_4bit = True, # False for LoRA 16bit
|
load_in_4bit = True, # False for LoRA 16bit
|
||||||
fast_inference = True, # Enable vLLM fast inference
|
fast_inference = True, # Enable vLLM fast inference
|
||||||
max_lora_rank = lora_rank,
|
max_lora_rank = lora_rank,
|
||||||
gpu_memory_utilization = 0.85, # Reduce if out of memory
|
gpu_memory_utilization = 0.5, # Reduce if out of memory
|
||||||
)
|
)
|
||||||
|
|
||||||
model = FastLanguageModel.get_peft_model(
|
model = FastLanguageModel.get_peft_model(
|
||||||
@@ -97,12 +98,6 @@ def main():
|
|||||||
loftq_config = None, # And LoftQ
|
loftq_config = None, # And LoftQ
|
||||||
)
|
)
|
||||||
|
|
||||||
response_template = tokenizer.apply_chat_template(
|
|
||||||
[{"role": "assistant", "content": ""}],
|
|
||||||
tokenize=False,
|
|
||||||
add_generation_prompt=True
|
|
||||||
)
|
|
||||||
|
|
||||||
training_args = TrainingArguments(
|
training_args = TrainingArguments(
|
||||||
output_dir=str(args.output_dir),
|
output_dir=str(args.output_dir),
|
||||||
num_train_epochs=3,
|
num_train_epochs=3,
|
||||||
@@ -129,10 +124,6 @@ def main():
|
|||||||
train_dataset=dataset.to_transformers_dataset(tokenizer),
|
train_dataset=dataset.to_transformers_dataset(tokenizer),
|
||||||
dataset_text_field="messages",
|
dataset_text_field="messages",
|
||||||
max_seq_length=max_seq_length,
|
max_seq_length=max_seq_length,
|
||||||
data_collator=DataCollatorForCompletionOnlyLM(
|
|
||||||
response_template=response_template,
|
|
||||||
tokenizer=tokenizer
|
|
||||||
),
|
|
||||||
)
|
)
|
||||||
|
|
||||||
trainer.train()
|
trainer.train()
|
||||||
@@ -140,7 +131,6 @@ def main():
|
|||||||
model.save_pretrained_merged(
|
model.save_pretrained_merged(
|
||||||
str(args.output_dir),
|
str(args.output_dir),
|
||||||
tokenizer=tokenizer,
|
tokenizer=tokenizer,
|
||||||
save_method="merged_16bit"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
|
|||||||
@@ -22,7 +22,7 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"cell_type": "code",
|
"cell_type": "code",
|
||||||
"execution_count": 2,
|
"execution_count": null,
|
||||||
"metadata": {},
|
"metadata": {},
|
||||||
"outputs": [],
|
"outputs": [],
|
||||||
"source": [
|
"source": [
|
||||||
|
|||||||
Reference in New Issue
Block a user