chore: import upstream snapshot with attribution
Book-CI / test (macos-latest) (push) Has been cancelled
Book-CI / test (ubuntu-latest) (push) Has been cancelled
Book-CI / test (windows-latest) (push) Has been cancelled
Release Fake Tag / publish (push) Has been cancelled
Deploy / deploy (macos-latest) (push) Has been cancelled
Deploy / deploy (ubuntu-latest) (push) Has been cancelled
Deploy / deploy (windows-latest) (push) Has been cancelled
Release to PyPI / Build & publish sglang-kt (push) Has been cancelled
Release to PyPI / Build kt-kernel (Python 3.11) (push) Has been cancelled
Release to PyPI / Build kt-kernel (Python 3.12) (push) Has been cancelled
Release to PyPI / Publish kt-kernel to PyPI (push) Has been cancelled
Book-CI / test (macos-latest) (push) Has been cancelled
Book-CI / test (ubuntu-latest) (push) Has been cancelled
Book-CI / test (windows-latest) (push) Has been cancelled
Release Fake Tag / publish (push) Has been cancelled
Deploy / deploy (macos-latest) (push) Has been cancelled
Deploy / deploy (ubuntu-latest) (push) Has been cancelled
Deploy / deploy (windows-latest) (push) Has been cancelled
Release to PyPI / Build & publish sglang-kt (push) Has been cancelled
Release to PyPI / Build kt-kernel (Python 3.11) (push) Has been cancelled
Release to PyPI / Build kt-kernel (Python 3.12) (push) Has been cancelled
Release to PyPI / Publish kt-kernel to PyPI (push) Has been cancelled
This commit is contained in:
@@ -0,0 +1,3 @@
|
||||
"""
|
||||
Command modules for kt-cli.
|
||||
"""
|
||||
@@ -0,0 +1,274 @@
|
||||
"""
|
||||
Bench commands for kt-cli.
|
||||
|
||||
Runs benchmarks for performance testing.
|
||||
"""
|
||||
|
||||
import subprocess
|
||||
import sys
|
||||
from enum import Enum
|
||||
from pathlib import Path
|
||||
from typing import Optional
|
||||
|
||||
import typer
|
||||
|
||||
from kt_kernel.cli.i18n import t
|
||||
from kt_kernel.cli.utils.console import (
|
||||
console,
|
||||
print_error,
|
||||
print_info,
|
||||
print_step,
|
||||
print_success,
|
||||
)
|
||||
|
||||
|
||||
class BenchType(str, Enum):
|
||||
"""Benchmark type."""
|
||||
|
||||
INFERENCE = "inference"
|
||||
MLA = "mla"
|
||||
MOE = "moe"
|
||||
LINEAR = "linear"
|
||||
ATTENTION = "attention"
|
||||
ALL = "all"
|
||||
|
||||
|
||||
def bench(
|
||||
type: BenchType = typer.Option(
|
||||
BenchType.ALL,
|
||||
"--type",
|
||||
"-t",
|
||||
help="Benchmark type",
|
||||
),
|
||||
model: Optional[str] = typer.Option(
|
||||
None,
|
||||
"--model",
|
||||
"-m",
|
||||
help="Model to benchmark",
|
||||
),
|
||||
output: Optional[Path] = typer.Option(
|
||||
None,
|
||||
"--output",
|
||||
"-o",
|
||||
help="Output file for results (JSON)",
|
||||
),
|
||||
iterations: int = typer.Option(
|
||||
10,
|
||||
"--iterations",
|
||||
"-n",
|
||||
help="Number of iterations",
|
||||
),
|
||||
) -> None:
|
||||
"""Run full benchmark suite."""
|
||||
console.print()
|
||||
print_step(t("bench_starting"))
|
||||
print_info(t("bench_type", type=type.value))
|
||||
console.print()
|
||||
|
||||
if type == BenchType.ALL:
|
||||
_run_all_benchmarks(model, output, iterations)
|
||||
elif type == BenchType.INFERENCE:
|
||||
_run_inference_benchmark(model, output, iterations)
|
||||
elif type == BenchType.MLA:
|
||||
_run_component_benchmark("mla", output, iterations)
|
||||
elif type == BenchType.MOE:
|
||||
_run_component_benchmark("moe", output, iterations)
|
||||
elif type == BenchType.LINEAR:
|
||||
_run_component_benchmark("linear", output, iterations)
|
||||
elif type == BenchType.ATTENTION:
|
||||
_run_component_benchmark("attention", output, iterations)
|
||||
|
||||
console.print()
|
||||
print_success(t("bench_complete"))
|
||||
if output:
|
||||
console.print(f" Results saved to: {output}")
|
||||
console.print()
|
||||
|
||||
|
||||
def microbench(
|
||||
component: str = typer.Argument(
|
||||
"moe",
|
||||
help="Component to benchmark (moe, mla, linear, attention)",
|
||||
),
|
||||
batch_size: int = typer.Option(
|
||||
1,
|
||||
"--batch-size",
|
||||
"-b",
|
||||
help="Batch size",
|
||||
),
|
||||
seq_len: int = typer.Option(
|
||||
1,
|
||||
"--seq-len",
|
||||
"-s",
|
||||
help="Sequence length",
|
||||
),
|
||||
iterations: int = typer.Option(
|
||||
100,
|
||||
"--iterations",
|
||||
"-n",
|
||||
help="Number of iterations",
|
||||
),
|
||||
warmup: int = typer.Option(
|
||||
10,
|
||||
"--warmup",
|
||||
"-w",
|
||||
help="Warmup iterations",
|
||||
),
|
||||
output: Optional[Path] = typer.Option(
|
||||
None,
|
||||
"--output",
|
||||
"-o",
|
||||
help="Output file for results (JSON)",
|
||||
),
|
||||
) -> None:
|
||||
"""Run micro-benchmark for specific components."""
|
||||
console.print()
|
||||
console.print(f"[yellow]{t('feature_coming_soon')}[/yellow]")
|
||||
console.print()
|
||||
raise typer.Exit(0)
|
||||
|
||||
# Try to find the benchmark script
|
||||
kt_kernel_path = _find_kt_kernel_path()
|
||||
|
||||
if kt_kernel_path is None:
|
||||
print_error("kt-kernel not found. Install with: kt install inference")
|
||||
raise typer.Exit(1)
|
||||
|
||||
bench_dir = kt_kernel_path / "bench"
|
||||
|
||||
# Map component to script
|
||||
component_scripts = {
|
||||
"moe": "bench_moe.py",
|
||||
"mla": "bench_mla.py",
|
||||
"linear": "bench_linear.py",
|
||||
"attention": "bench_attention.py",
|
||||
"mlp": "bench_mlp.py",
|
||||
}
|
||||
|
||||
script_name = component_scripts.get(component.lower())
|
||||
if script_name is None:
|
||||
print_error(f"Unknown component: {component}")
|
||||
console.print(f"Available: {', '.join(component_scripts.keys())}")
|
||||
raise typer.Exit(1)
|
||||
|
||||
script_path = bench_dir / script_name
|
||||
if not script_path.exists():
|
||||
print_error(f"Benchmark script not found: {script_path}")
|
||||
raise typer.Exit(1)
|
||||
|
||||
# Run benchmark
|
||||
cmd = [
|
||||
sys.executable,
|
||||
str(script_path),
|
||||
"--batch-size",
|
||||
str(batch_size),
|
||||
"--seq-len",
|
||||
str(seq_len),
|
||||
"--iterations",
|
||||
str(iterations),
|
||||
"--warmup",
|
||||
str(warmup),
|
||||
]
|
||||
|
||||
if output:
|
||||
cmd.extend(["--output", str(output)])
|
||||
|
||||
console.print(f"[dim]$ {' '.join(cmd)}[/dim]")
|
||||
console.print()
|
||||
|
||||
try:
|
||||
process = subprocess.run(cmd)
|
||||
|
||||
if process.returncode == 0:
|
||||
console.print()
|
||||
print_success(t("bench_complete"))
|
||||
if output:
|
||||
console.print(f" Results saved to: {output}")
|
||||
else:
|
||||
print_error(f"Benchmark failed with exit code {process.returncode}")
|
||||
raise typer.Exit(process.returncode)
|
||||
|
||||
except FileNotFoundError as e:
|
||||
print_error(f"Failed to run benchmark: {e}")
|
||||
raise typer.Exit(1)
|
||||
|
||||
|
||||
def _find_kt_kernel_path() -> Optional[Path]:
|
||||
"""Find the kt-kernel installation path."""
|
||||
try:
|
||||
import kt_kernel
|
||||
|
||||
return Path(kt_kernel.__file__).parent.parent
|
||||
except ImportError:
|
||||
pass
|
||||
|
||||
# Check common locations
|
||||
possible_paths = [
|
||||
Path.home() / "Projects" / "ktransformers" / "kt-kernel",
|
||||
Path("/opt/ktransformers/kt-kernel"),
|
||||
Path.cwd() / "kt-kernel",
|
||||
]
|
||||
|
||||
for path in possible_paths:
|
||||
if path.exists() and (path / "bench").exists():
|
||||
return path
|
||||
|
||||
return None
|
||||
|
||||
|
||||
def _run_all_benchmarks(model: Optional[str], output: Optional[Path], iterations: int) -> None:
|
||||
"""Run all benchmarks."""
|
||||
components = ["moe", "mla", "linear", "attention"]
|
||||
|
||||
for component in components:
|
||||
console.print(f"\n[bold]Running {component} benchmark...[/bold]")
|
||||
_run_component_benchmark(component, None, iterations)
|
||||
|
||||
|
||||
def _run_inference_benchmark(model: Optional[str], output: Optional[Path], iterations: int) -> None:
|
||||
"""Run inference benchmark."""
|
||||
if model is None:
|
||||
print_error("Model required for inference benchmark. Use --model flag.")
|
||||
raise typer.Exit(1)
|
||||
|
||||
print_info(f"Running inference benchmark on {model}...")
|
||||
console.print()
|
||||
console.print("[dim]This will start the server and run test requests.[/dim]")
|
||||
console.print()
|
||||
|
||||
# TODO: Implement actual inference benchmarking
|
||||
print_error("Inference benchmarking not yet implemented.")
|
||||
|
||||
|
||||
def _run_component_benchmark(component: str, output: Optional[Path], iterations: int) -> None:
|
||||
"""Run a component benchmark."""
|
||||
kt_kernel_path = _find_kt_kernel_path()
|
||||
|
||||
if kt_kernel_path is None:
|
||||
print_error("kt-kernel not found.")
|
||||
return
|
||||
|
||||
bench_dir = kt_kernel_path / "bench"
|
||||
script_map = {
|
||||
"moe": "bench_moe.py",
|
||||
"mla": "bench_mla.py",
|
||||
"linear": "bench_linear.py",
|
||||
"attention": "bench_attention.py",
|
||||
}
|
||||
|
||||
script_name = script_map.get(component)
|
||||
if script_name is None:
|
||||
print_error(f"Unknown component: {component}")
|
||||
return
|
||||
|
||||
script_path = bench_dir / script_name
|
||||
if not script_path.exists():
|
||||
print_error(f"Script not found: {script_path}")
|
||||
return
|
||||
|
||||
cmd = [sys.executable, str(script_path), "--iterations", str(iterations)]
|
||||
|
||||
try:
|
||||
subprocess.run(cmd)
|
||||
except Exception as e:
|
||||
print_error(f"Benchmark failed: {e}")
|
||||
@@ -0,0 +1,572 @@
|
||||
"""
|
||||
Chat command for kt-cli.
|
||||
|
||||
Provides interactive chat interface with running model server.
|
||||
"""
|
||||
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
from datetime import datetime
|
||||
from pathlib import Path
|
||||
from typing import Optional
|
||||
|
||||
import typer
|
||||
from rich.console import Console
|
||||
from rich.markdown import Markdown
|
||||
from rich.panel import Panel
|
||||
from rich.prompt import Prompt, Confirm
|
||||
|
||||
from kt_kernel.cli.config.settings import get_settings
|
||||
from kt_kernel.cli.i18n import t
|
||||
from kt_kernel.cli.utils.console import (
|
||||
console,
|
||||
print_error,
|
||||
print_info,
|
||||
print_success,
|
||||
print_warning,
|
||||
)
|
||||
|
||||
# Try to import OpenAI SDK
|
||||
try:
|
||||
from openai import OpenAI
|
||||
|
||||
HAS_OPENAI = True
|
||||
except ImportError:
|
||||
HAS_OPENAI = False
|
||||
|
||||
|
||||
def chat(
|
||||
host: Optional[str] = typer.Option(
|
||||
None,
|
||||
"--host",
|
||||
"-H",
|
||||
help="Server host address",
|
||||
),
|
||||
port: Optional[int] = typer.Option(
|
||||
None,
|
||||
"--port",
|
||||
"-p",
|
||||
help="Server port",
|
||||
),
|
||||
model: Optional[str] = typer.Option(
|
||||
None,
|
||||
"--model",
|
||||
"-m",
|
||||
help="Model name (if server hosts multiple models)",
|
||||
),
|
||||
temperature: float = typer.Option(
|
||||
0.7,
|
||||
"--temperature",
|
||||
"-t",
|
||||
help="Sampling temperature (0.0 to 2.0)",
|
||||
),
|
||||
max_tokens: int = typer.Option(
|
||||
2048,
|
||||
"--max-tokens",
|
||||
help="Maximum tokens to generate",
|
||||
),
|
||||
system_prompt: Optional[str] = typer.Option(
|
||||
None,
|
||||
"--system",
|
||||
"-s",
|
||||
help="System prompt",
|
||||
),
|
||||
save_history: bool = typer.Option(
|
||||
True,
|
||||
"--save-history/--no-save-history",
|
||||
help="Save conversation history",
|
||||
),
|
||||
history_file: Optional[Path] = typer.Option(
|
||||
None,
|
||||
"--history-file",
|
||||
help="Path to save conversation history",
|
||||
),
|
||||
stream: bool = typer.Option(
|
||||
True,
|
||||
"--stream/--no-stream",
|
||||
help="Enable streaming output",
|
||||
),
|
||||
) -> None:
|
||||
"""Start interactive chat with a running model server.
|
||||
|
||||
Examples:
|
||||
kt chat # Connect to default server
|
||||
kt chat --host 127.0.0.1 -p 8080 # Connect to specific server
|
||||
kt chat -t 0.9 --max-tokens 4096 # Adjust generation parameters
|
||||
"""
|
||||
if not HAS_OPENAI:
|
||||
print_error(t("chat_openai_required"))
|
||||
console.print()
|
||||
console.print(t("chat_install_hint"))
|
||||
console.print(" pip install openai")
|
||||
raise typer.Exit(1)
|
||||
|
||||
settings = get_settings()
|
||||
|
||||
# Resolve server connection
|
||||
final_host = host or settings.get("server.host", "127.0.0.1")
|
||||
final_port = port or settings.get("server.port", 30000)
|
||||
|
||||
# Construct base URL for OpenAI-compatible API
|
||||
base_url = f"http://{final_host}:{final_port}/v1"
|
||||
|
||||
console.print()
|
||||
console.print(
|
||||
Panel.fit(
|
||||
f"[bold cyan]{t('chat_title')}[/bold cyan]\n\n"
|
||||
f"{t('chat_server')}: [yellow]{final_host}:{final_port}[/yellow]\n"
|
||||
f"{t('chat_temperature')}: [cyan]{temperature}[/cyan] | {t('chat_max_tokens')}: [cyan]{max_tokens}[/cyan]\n\n"
|
||||
f"[dim]{t('chat_help_hint')}[/dim]",
|
||||
border_style="cyan",
|
||||
)
|
||||
)
|
||||
console.print()
|
||||
|
||||
# Check for proxy environment variables
|
||||
proxy_vars = ["HTTP_PROXY", "HTTPS_PROXY", "http_proxy", "https_proxy", "ALL_PROXY", "all_proxy"]
|
||||
detected_proxies = {var: os.environ.get(var) for var in proxy_vars if os.environ.get(var)}
|
||||
|
||||
if detected_proxies:
|
||||
proxy_info = ", ".join(f"{k}={v}" for k, v in detected_proxies.items())
|
||||
console.print()
|
||||
print_warning(t("chat_proxy_detected"))
|
||||
console.print(f" [dim]{proxy_info}[/dim]")
|
||||
console.print()
|
||||
|
||||
use_proxy = Confirm.ask(t("chat_proxy_confirm"), default=False)
|
||||
|
||||
if not use_proxy:
|
||||
# Temporarily disable proxy for this connection
|
||||
for var in proxy_vars:
|
||||
if var in os.environ:
|
||||
del os.environ[var]
|
||||
print_info(t("chat_proxy_disabled"))
|
||||
console.print()
|
||||
|
||||
# Initialize OpenAI client
|
||||
try:
|
||||
client = OpenAI(
|
||||
base_url=base_url,
|
||||
api_key="EMPTY", # SGLang doesn't require API key
|
||||
)
|
||||
|
||||
# Test connection
|
||||
print_info(t("chat_connecting"))
|
||||
models = client.models.list()
|
||||
available_models = [m.id for m in models.data]
|
||||
|
||||
if not available_models:
|
||||
print_error(t("chat_no_models"))
|
||||
raise typer.Exit(1)
|
||||
|
||||
# Select model
|
||||
if model:
|
||||
if model not in available_models:
|
||||
print_warning(t("chat_model_not_found", model=model, available=", ".join(available_models)))
|
||||
selected_model = available_models[0]
|
||||
else:
|
||||
selected_model = model
|
||||
else:
|
||||
selected_model = available_models[0]
|
||||
|
||||
print_success(t("chat_connected", model=selected_model))
|
||||
console.print()
|
||||
|
||||
# Load tokenizer for accurate token counting
|
||||
tokenizer = None
|
||||
try:
|
||||
from transformers import AutoTokenizer
|
||||
|
||||
# selected_model is the model path
|
||||
tokenizer = AutoTokenizer.from_pretrained(selected_model, trust_remote_code=True)
|
||||
console.print(f"[dim]Loaded tokenizer from {selected_model}[/dim]")
|
||||
console.print()
|
||||
except Exception as e:
|
||||
console.print(f"[dim yellow]Warning: Could not load tokenizer, token counts will be estimated[/dim]")
|
||||
console.print()
|
||||
|
||||
except Exception as e:
|
||||
print_error(t("chat_connect_failed", error=str(e)))
|
||||
console.print()
|
||||
console.print(t("chat_server_not_running"))
|
||||
console.print(" kt run <model>")
|
||||
raise typer.Exit(1)
|
||||
|
||||
# Initialize conversation history
|
||||
messages = []
|
||||
|
||||
# Add system prompt if provided
|
||||
if system_prompt:
|
||||
messages.append({"role": "system", "content": system_prompt})
|
||||
|
||||
# Setup history file
|
||||
if save_history:
|
||||
if history_file is None:
|
||||
history_dir = settings.config_dir / "chat_history"
|
||||
history_dir.mkdir(parents=True, exist_ok=True)
|
||||
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
|
||||
history_file = history_dir / f"chat_{timestamp}.json"
|
||||
else:
|
||||
history_file = Path(history_file)
|
||||
history_file.parent.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
# Main chat loop
|
||||
try:
|
||||
while True:
|
||||
# Get user input - use console.input() for better CJK character support
|
||||
try:
|
||||
user_input = console.input(f"[bold green]{t('chat_user_prompt')}[/bold green]: ")
|
||||
except (EOFError, KeyboardInterrupt):
|
||||
console.print()
|
||||
print_info(t("chat_goodbye"))
|
||||
break
|
||||
|
||||
if not user_input.strip():
|
||||
continue
|
||||
|
||||
# Handle special commands
|
||||
if user_input.startswith("/"):
|
||||
if _handle_command(user_input, messages, temperature, max_tokens):
|
||||
continue
|
||||
else:
|
||||
break # Exit command
|
||||
|
||||
# Add user message to history
|
||||
messages.append({"role": "user", "content": user_input})
|
||||
|
||||
# Generate response
|
||||
console.print()
|
||||
console.print(f"[bold cyan]{t('chat_assistant_prompt')}[/bold cyan]")
|
||||
|
||||
try:
|
||||
if stream:
|
||||
# Streaming response
|
||||
response_content = _stream_response(
|
||||
client, selected_model, messages, temperature, max_tokens, tokenizer
|
||||
)
|
||||
else:
|
||||
# Non-streaming response
|
||||
response_content = _generate_response(
|
||||
client, selected_model, messages, temperature, max_tokens, tokenizer
|
||||
)
|
||||
|
||||
# Add assistant response to history
|
||||
messages.append({"role": "assistant", "content": response_content})
|
||||
|
||||
console.print()
|
||||
|
||||
except Exception as e:
|
||||
print_error(t("chat_generation_error", error=str(e)))
|
||||
# Remove the user message that caused the error
|
||||
messages.pop()
|
||||
continue
|
||||
|
||||
# Save history if enabled
|
||||
if save_history:
|
||||
_save_history(history_file, messages, selected_model)
|
||||
|
||||
except KeyboardInterrupt:
|
||||
console.print()
|
||||
console.print()
|
||||
print_info(t("chat_interrupted"))
|
||||
|
||||
# Final history save
|
||||
if save_history and messages:
|
||||
_save_history(history_file, messages, selected_model)
|
||||
console.print(f"[dim]{t('chat_history_saved', path=str(history_file))}[/dim]")
|
||||
console.print()
|
||||
|
||||
|
||||
def _stream_response(
|
||||
client: "OpenAI",
|
||||
model: str,
|
||||
messages: list,
|
||||
temperature: float,
|
||||
max_tokens: int,
|
||||
tokenizer=None,
|
||||
) -> str:
|
||||
"""Generate streaming response and display in real-time."""
|
||||
import time
|
||||
|
||||
response_content = ""
|
||||
reasoning_content = ""
|
||||
|
||||
# Performance tracking
|
||||
first_token_time = None
|
||||
chunk_count = 0
|
||||
|
||||
try:
|
||||
# Start timing before sending request
|
||||
start_time = time.time()
|
||||
|
||||
stream = client.chat.completions.create(
|
||||
model=model,
|
||||
messages=messages,
|
||||
temperature=temperature,
|
||||
max_tokens=max_tokens,
|
||||
stream=True,
|
||||
)
|
||||
|
||||
for chunk in stream:
|
||||
delta = chunk.choices[0].delta if chunk.choices else None
|
||||
if delta:
|
||||
reasoning_delta = getattr(delta, "reasoning_content", None)
|
||||
if reasoning_delta:
|
||||
if first_token_time is None:
|
||||
first_token_time = time.time()
|
||||
reasoning_content += reasoning_delta
|
||||
console.print(reasoning_delta, end="", style="dim")
|
||||
chunk_count += 1
|
||||
|
||||
if delta.content:
|
||||
if first_token_time is None:
|
||||
first_token_time = time.time()
|
||||
content = delta.content
|
||||
response_content += content
|
||||
console.print(content, end="")
|
||||
chunk_count += 1
|
||||
|
||||
console.print() # Newline after streaming
|
||||
|
||||
# Display performance metrics
|
||||
end_time = time.time()
|
||||
if first_token_time and chunk_count > 0:
|
||||
ttft = first_token_time - start_time
|
||||
total_time = end_time - start_time
|
||||
|
||||
# Calculate TPOT based on chunks
|
||||
if chunk_count > 1:
|
||||
generation_time = total_time - ttft
|
||||
tpot = generation_time / (chunk_count - 1)
|
||||
else:
|
||||
tpot = 0
|
||||
|
||||
# Calculate accurate token counts using tokenizer
|
||||
if tokenizer:
|
||||
input_tokens = _count_tokens_with_tokenizer(messages, tokenizer)
|
||||
output_tokens = _count_tokens_with_tokenizer(
|
||||
[{"role": "assistant", "content": response_content}], tokenizer
|
||||
)
|
||||
token_prefix = ""
|
||||
else:
|
||||
# Fallback to estimation
|
||||
input_tokens = _estimate_tokens(messages)
|
||||
output_tokens = _estimate_tokens([{"role": "assistant", "content": response_content}])
|
||||
token_prefix = "~"
|
||||
|
||||
# Build metrics display
|
||||
metrics = f"[dim]Total: {total_time*1000:.0f}ms | TTFT: {ttft*1000:.0f}ms"
|
||||
if tpot > 0:
|
||||
metrics += f" | TPOT: {tpot*1000:.1f}ms"
|
||||
metrics += f" | In: {token_prefix}{input_tokens} | Out: {token_prefix}{output_tokens}"
|
||||
metrics += "[/dim]"
|
||||
|
||||
console.print(metrics)
|
||||
|
||||
except Exception as e:
|
||||
raise Exception(f"Streaming error: {e}")
|
||||
|
||||
return response_content
|
||||
|
||||
|
||||
def _count_tokens_with_tokenizer(messages: list, tokenizer) -> int:
|
||||
"""Count tokens accurately using the model's tokenizer."""
|
||||
try:
|
||||
# Concatenate all message content
|
||||
text = ""
|
||||
for msg in messages:
|
||||
role = msg.get("role", "")
|
||||
content = msg.get("content", "")
|
||||
# Simple format: role + content
|
||||
text += f"{role}: {content}\n"
|
||||
|
||||
# Encode and count tokens - suppress any debug output from custom tokenizers
|
||||
import os
|
||||
import sys
|
||||
from contextlib import redirect_stdout, redirect_stderr
|
||||
|
||||
with open(os.devnull, "w") as devnull:
|
||||
with redirect_stdout(devnull), redirect_stderr(devnull):
|
||||
tokens = tokenizer.encode(text, add_special_tokens=True)
|
||||
return len(tokens)
|
||||
except Exception:
|
||||
# Fallback to estimation if tokenizer fails
|
||||
return _estimate_tokens(messages)
|
||||
|
||||
|
||||
def _estimate_tokens(messages: list) -> int:
|
||||
"""Estimate token count for messages (rough approximation)."""
|
||||
total_chars = 0
|
||||
for msg in messages:
|
||||
content = msg.get("content", "")
|
||||
total_chars += len(content)
|
||||
|
||||
# Rough estimation:
|
||||
# - English: ~4 chars per token
|
||||
# - Chinese: ~1.5 chars per token
|
||||
# Use 2.5 as average
|
||||
return max(1, int(total_chars / 2.5))
|
||||
|
||||
|
||||
def _generate_response(
|
||||
client: "OpenAI",
|
||||
model: str,
|
||||
messages: list,
|
||||
temperature: float,
|
||||
max_tokens: int,
|
||||
tokenizer=None,
|
||||
) -> str:
|
||||
"""Generate non-streaming response."""
|
||||
import time
|
||||
|
||||
try:
|
||||
start_time = time.time()
|
||||
|
||||
response = client.chat.completions.create(
|
||||
model=model,
|
||||
messages=messages,
|
||||
temperature=temperature,
|
||||
max_tokens=max_tokens,
|
||||
stream=False,
|
||||
)
|
||||
|
||||
end_time = time.time()
|
||||
total_time = end_time - start_time
|
||||
|
||||
content = response.choices[0].message.content
|
||||
|
||||
# Display as markdown
|
||||
md = Markdown(content)
|
||||
console.print(md)
|
||||
|
||||
# Calculate accurate token counts using tokenizer
|
||||
if tokenizer:
|
||||
input_tokens = _count_tokens_with_tokenizer(messages, tokenizer)
|
||||
output_tokens = _count_tokens_with_tokenizer([{"role": "assistant", "content": content}], tokenizer)
|
||||
token_prefix = ""
|
||||
else:
|
||||
# Fallback to API usage or estimation
|
||||
input_tokens = response.usage.prompt_tokens if response.usage else _estimate_tokens(messages)
|
||||
output_tokens = (
|
||||
response.usage.completion_tokens
|
||||
if response.usage
|
||||
else _estimate_tokens([{"role": "assistant", "content": content}])
|
||||
)
|
||||
token_prefix = "" if response.usage else "~"
|
||||
|
||||
# Display performance metrics
|
||||
console.print(
|
||||
f"[dim]Time: {total_time*1000:.0f}ms | "
|
||||
f"In: {token_prefix}{input_tokens} | Out: {token_prefix}{output_tokens}[/dim]"
|
||||
)
|
||||
|
||||
return content
|
||||
|
||||
except Exception as e:
|
||||
raise Exception(f"Generation error: {e}")
|
||||
|
||||
|
||||
def _handle_command(command: str, messages: list, temperature: float, max_tokens: int) -> bool:
|
||||
"""Handle special commands. Returns True to continue chat, False to exit."""
|
||||
cmd = command.lower().strip()
|
||||
|
||||
if cmd in ["/quit", "/exit", "/q"]:
|
||||
console.print()
|
||||
print_info(t("chat_goodbye"))
|
||||
return False
|
||||
|
||||
elif cmd in ["/help", "/h"]:
|
||||
console.print()
|
||||
console.print(
|
||||
Panel(
|
||||
f"[bold]{t('chat_help_title')}[/bold]\n\n{t('chat_help_content')}",
|
||||
title="Help",
|
||||
border_style="cyan",
|
||||
)
|
||||
)
|
||||
console.print()
|
||||
return True
|
||||
|
||||
elif cmd in ["/clear", "/c"]:
|
||||
messages.clear()
|
||||
console.print()
|
||||
print_success(t("chat_history_cleared"))
|
||||
console.print()
|
||||
return True
|
||||
|
||||
elif cmd in ["/history", "/hist"]:
|
||||
console.print()
|
||||
if not messages:
|
||||
print_info(t("chat_no_history"))
|
||||
else:
|
||||
console.print(
|
||||
Panel(
|
||||
_format_history(messages),
|
||||
title=t("chat_history_title", count=len(messages)),
|
||||
border_style="cyan",
|
||||
)
|
||||
)
|
||||
console.print()
|
||||
return True
|
||||
|
||||
elif cmd in ["/info", "/i"]:
|
||||
console.print()
|
||||
console.print(
|
||||
Panel(
|
||||
f"[bold]{t('chat_info_title')}[/bold]\n\n{t('chat_info_content', temperature=temperature, max_tokens=max_tokens, messages=len(messages))}",
|
||||
title="Info",
|
||||
border_style="cyan",
|
||||
)
|
||||
)
|
||||
console.print()
|
||||
return True
|
||||
|
||||
elif cmd in ["/retry", "/r"]:
|
||||
if len(messages) >= 2 and messages[-1]["role"] == "assistant":
|
||||
# Remove last assistant response
|
||||
messages.pop()
|
||||
print_info(t("chat_retrying"))
|
||||
console.print()
|
||||
else:
|
||||
print_warning(t("chat_no_retry"))
|
||||
console.print()
|
||||
return True
|
||||
|
||||
else:
|
||||
print_warning(t("chat_unknown_command", command=command))
|
||||
console.print(f"[dim]{t('chat_unknown_hint')}[/dim]")
|
||||
console.print()
|
||||
return True
|
||||
|
||||
|
||||
def _format_history(messages: list) -> str:
|
||||
"""Format conversation history for display."""
|
||||
lines = []
|
||||
for i, msg in enumerate(messages, 1):
|
||||
role = msg["role"].capitalize()
|
||||
content = msg["content"]
|
||||
|
||||
# Truncate long messages
|
||||
if len(content) > 200:
|
||||
content = content[:200] + "..."
|
||||
|
||||
lines.append(f"[bold]{i}. {role}:[/bold] {content}")
|
||||
|
||||
return "\n\n".join(lines)
|
||||
|
||||
|
||||
def _save_history(file_path: Path, messages: list, model: str) -> None:
|
||||
"""Save conversation history to file."""
|
||||
try:
|
||||
history_data = {
|
||||
"model": model,
|
||||
"timestamp": datetime.now().isoformat(),
|
||||
"messages": messages,
|
||||
}
|
||||
|
||||
with open(file_path, "w", encoding="utf-8") as f:
|
||||
json.dump(history_data, f, indent=2, ensure_ascii=False)
|
||||
|
||||
except Exception as e:
|
||||
print_warning(f"Failed to save history: {e}")
|
||||
@@ -0,0 +1,167 @@
|
||||
"""
|
||||
Config command for kt-cli.
|
||||
|
||||
Manages kt-cli configuration.
|
||||
"""
|
||||
|
||||
from typing import Optional
|
||||
|
||||
import typer
|
||||
import yaml
|
||||
from rich.syntax import Syntax
|
||||
|
||||
from kt_kernel.cli.config.settings import get_settings
|
||||
from kt_kernel.cli.i18n import t
|
||||
from kt_kernel.cli.utils.console import confirm, console, print_error, print_success
|
||||
|
||||
app = typer.Typer(help="Manage kt-cli configuration")
|
||||
|
||||
|
||||
@app.command(name="init")
|
||||
def init() -> None:
|
||||
"""Initialize or re-run the first-time setup wizard."""
|
||||
from kt_kernel.cli.main import _show_first_run_setup
|
||||
from kt_kernel.cli.config.settings import get_settings
|
||||
|
||||
settings = get_settings()
|
||||
_show_first_run_setup(settings)
|
||||
|
||||
|
||||
@app.command(name="show")
|
||||
def show(
|
||||
key: Optional[str] = typer.Argument(None, help="Configuration key to show (e.g., server.port)"),
|
||||
) -> None:
|
||||
"""Show current configuration."""
|
||||
settings = get_settings()
|
||||
|
||||
if key:
|
||||
value = settings.get(key)
|
||||
if value is not None:
|
||||
if isinstance(value, (dict, list)):
|
||||
console.print(yaml.dump({key: value}, default_flow_style=False, allow_unicode=True))
|
||||
else:
|
||||
console.print(t("config_get_value", key=key, value=value))
|
||||
else:
|
||||
print_error(t("config_get_not_found", key=key))
|
||||
raise typer.Exit(1)
|
||||
else:
|
||||
console.print(f"\n[bold]{t('config_show_title')}[/bold]\n")
|
||||
console.print(f"[dim]{t('config_file_location', path=str(settings.config_path))}[/dim]\n")
|
||||
|
||||
config_yaml = yaml.dump(settings.get_all(), default_flow_style=False, allow_unicode=True)
|
||||
syntax = Syntax(config_yaml, "yaml", theme="monokai", line_numbers=False)
|
||||
console.print(syntax)
|
||||
|
||||
|
||||
@app.command(name="set")
|
||||
def set_config(
|
||||
key: str = typer.Argument(..., help="Configuration key (e.g., server.port)"),
|
||||
value: str = typer.Argument(..., help="Value to set"),
|
||||
) -> None:
|
||||
"""Set a configuration value."""
|
||||
settings = get_settings()
|
||||
|
||||
# Try to parse value as JSON/YAML for complex types
|
||||
parsed_value = _parse_value(value)
|
||||
|
||||
settings.set(key, parsed_value)
|
||||
print_success(t("config_set_success", key=key, value=parsed_value))
|
||||
|
||||
|
||||
@app.command(name="get")
|
||||
def get_config(
|
||||
key: str = typer.Argument(..., help="Configuration key (e.g., server.port)"),
|
||||
) -> None:
|
||||
"""Get a configuration value."""
|
||||
settings = get_settings()
|
||||
value = settings.get(key)
|
||||
|
||||
if value is not None:
|
||||
if isinstance(value, (dict, list)):
|
||||
console.print(yaml.dump(value, default_flow_style=False, allow_unicode=True))
|
||||
else:
|
||||
console.print(str(value))
|
||||
else:
|
||||
print_error(t("config_get_not_found", key=key))
|
||||
raise typer.Exit(1)
|
||||
|
||||
|
||||
@app.command(name="reset")
|
||||
def reset(
|
||||
yes: bool = typer.Option(False, "--yes", "-y", help="Skip confirmation"),
|
||||
) -> None:
|
||||
"""Reset configuration to defaults."""
|
||||
if not yes:
|
||||
if not confirm(t("config_reset_confirm"), default=False):
|
||||
raise typer.Abort()
|
||||
|
||||
settings = get_settings()
|
||||
settings.reset()
|
||||
print_success(t("config_reset_success"))
|
||||
|
||||
|
||||
@app.command(name="path")
|
||||
def path() -> None:
|
||||
"""Show configuration file path."""
|
||||
settings = get_settings()
|
||||
console.print(str(settings.config_path))
|
||||
|
||||
|
||||
@app.command(name="model-path-list", deprecated=True, hidden=True)
|
||||
def model_path_list() -> None:
|
||||
"""[Deprecated] Use 'kt model path-list' instead."""
|
||||
console.print("[yellow]⚠ This command is deprecated. Use 'kt model path-list' instead.[/yellow]\n")
|
||||
import subprocess
|
||||
subprocess.run(["kt", "model", "path-list"])
|
||||
|
||||
|
||||
@app.command(name="model-path-add", deprecated=True, hidden=True)
|
||||
def model_path_add(
|
||||
path: str = typer.Argument(..., help="Path to add"),
|
||||
) -> None:
|
||||
"""[Deprecated] Use 'kt model path-add' instead."""
|
||||
console.print("[yellow]⚠ This command is deprecated. Use 'kt model path-add' instead.[/yellow]\n")
|
||||
import subprocess
|
||||
subprocess.run(["kt", "model", "path-add", path])
|
||||
|
||||
|
||||
@app.command(name="model-path-remove", deprecated=True, hidden=True)
|
||||
def model_path_remove(
|
||||
path: str = typer.Argument(..., help="Path to remove"),
|
||||
) -> None:
|
||||
"""[Deprecated] Use 'kt model path-remove' instead."""
|
||||
console.print("[yellow]⚠ This command is deprecated. Use 'kt model path-remove' instead.[/yellow]\n")
|
||||
import subprocess
|
||||
subprocess.run(["kt", "model", "path-remove", path])
|
||||
|
||||
|
||||
def _parse_value(value: str):
|
||||
"""Parse a string value into appropriate Python type."""
|
||||
# Try boolean
|
||||
if value.lower() in ("true", "yes", "on", "1"):
|
||||
return True
|
||||
if value.lower() in ("false", "no", "off", "0"):
|
||||
return False
|
||||
|
||||
# Try integer
|
||||
try:
|
||||
return int(value)
|
||||
except ValueError:
|
||||
pass
|
||||
|
||||
# Try float
|
||||
try:
|
||||
return float(value)
|
||||
except ValueError:
|
||||
pass
|
||||
|
||||
# Try YAML/JSON parsing for lists/dicts
|
||||
try:
|
||||
parsed = yaml.safe_load(value)
|
||||
if isinstance(parsed, (dict, list)):
|
||||
return parsed
|
||||
except yaml.YAMLError:
|
||||
pass
|
||||
|
||||
# Return as string
|
||||
return value
|
||||
@@ -0,0 +1,556 @@
|
||||
"""
|
||||
Doctor command for kt-cli.
|
||||
|
||||
Diagnoses environment issues and provides recommendations.
|
||||
"""
|
||||
|
||||
import glob
|
||||
import os
|
||||
import platform
|
||||
import shutil
|
||||
from pathlib import Path
|
||||
from typing import Optional
|
||||
|
||||
import typer
|
||||
from rich.table import Table
|
||||
|
||||
from kt_kernel.cli.config.settings import get_settings
|
||||
from kt_kernel.cli.i18n import t
|
||||
from kt_kernel.cli.utils.console import console, print_error, print_info, print_success, print_warning
|
||||
from kt_kernel.cli.utils.environment import (
|
||||
check_docker,
|
||||
detect_available_ram_gb,
|
||||
detect_cpu_info,
|
||||
detect_cuda_version,
|
||||
detect_disk_space_gb,
|
||||
detect_env_managers,
|
||||
detect_gpus,
|
||||
detect_memory_info,
|
||||
detect_ram_gb,
|
||||
get_installed_package_version,
|
||||
)
|
||||
|
||||
|
||||
def _get_kt_kernel_info() -> dict:
|
||||
"""Get kt-kernel installation information."""
|
||||
info = {
|
||||
"installed": False,
|
||||
"version": None,
|
||||
"cpu_variant": None,
|
||||
"install_path": None,
|
||||
"available_variants": [],
|
||||
"extension_file": None,
|
||||
}
|
||||
|
||||
try:
|
||||
import kt_kernel
|
||||
|
||||
info["installed"] = True
|
||||
info["version"] = getattr(kt_kernel, "__version__", "unknown")
|
||||
info["cpu_variant"] = getattr(kt_kernel, "__cpu_variant__", "unknown")
|
||||
|
||||
# Get installation path
|
||||
info["install_path"] = os.path.dirname(kt_kernel.__file__)
|
||||
|
||||
# Find available .so files
|
||||
kt_kernel_dir = info["install_path"]
|
||||
so_files = glob.glob(os.path.join(kt_kernel_dir, "_kt_kernel_ext_*.so"))
|
||||
so_files.extend(glob.glob(os.path.join(kt_kernel_dir, "kt_kernel_ext*.so")))
|
||||
|
||||
# Parse variant names from filenames
|
||||
variants = set()
|
||||
for so_file in so_files:
|
||||
basename = os.path.basename(so_file)
|
||||
if "_kt_kernel_ext_" in basename:
|
||||
# Extract variant from _kt_kernel_ext_amx.cpython-311-x86_64-linux-gnu.so
|
||||
parts = basename.split("_")
|
||||
if len(parts) >= 4:
|
||||
variant = parts[3] # "amx" from "_kt_kernel_ext_amx..."
|
||||
if variant.startswith("avx"):
|
||||
# Normalize avx variants
|
||||
if variant in ["avx512", "avx512_bf16", "avx512_vbmi", "avx512_vnni", "avx512_base"]:
|
||||
variants.add("avx512")
|
||||
else:
|
||||
variants.add(variant)
|
||||
else:
|
||||
variants.add(variant)
|
||||
elif "kt_kernel_ext" in basename:
|
||||
variants.add("default")
|
||||
|
||||
info["available_variants"] = sorted(list(variants))
|
||||
|
||||
# Get current extension file
|
||||
if hasattr(kt_kernel, "kt_kernel_ext"):
|
||||
ext_module = kt_kernel.kt_kernel_ext
|
||||
info["extension_file"] = getattr(ext_module, "__file__", None)
|
||||
|
||||
except ImportError:
|
||||
info["installed"] = False
|
||||
except Exception as e:
|
||||
info["error"] = str(e)
|
||||
|
||||
return info
|
||||
|
||||
|
||||
def doctor(
|
||||
verbose: bool = typer.Option(False, "--verbose", "-v", help="Show detailed diagnostics"),
|
||||
) -> None:
|
||||
"""Diagnose environment issues."""
|
||||
console.print(f"\n[bold]{t('doctor_title')}[/bold]\n")
|
||||
|
||||
issues_found = False
|
||||
checks = []
|
||||
|
||||
# 1. Python version
|
||||
python_version = platform.python_version()
|
||||
python_ok = _check_python_version(python_version)
|
||||
checks.append(
|
||||
{
|
||||
"name": t("doctor_check_python"),
|
||||
"status": "ok" if python_ok else "error",
|
||||
"value": python_version,
|
||||
"hint": "Python 3.10+ required" if not python_ok else None,
|
||||
}
|
||||
)
|
||||
if not python_ok:
|
||||
issues_found = True
|
||||
|
||||
# 2. CUDA availability
|
||||
cuda_version = detect_cuda_version()
|
||||
checks.append(
|
||||
{
|
||||
"name": t("doctor_check_cuda"),
|
||||
"status": "ok" if cuda_version else "warning",
|
||||
"value": cuda_version or t("version_cuda_not_found"),
|
||||
"hint": "CUDA is optional but recommended for GPU acceleration" if not cuda_version else None,
|
||||
}
|
||||
)
|
||||
|
||||
# 3. GPU detection
|
||||
gpus = detect_gpus()
|
||||
if gpus:
|
||||
gpu_names = ", ".join(g.name for g in gpus)
|
||||
total_vram = sum(g.vram_gb for g in gpus)
|
||||
checks.append(
|
||||
{
|
||||
"name": t("doctor_check_gpu"),
|
||||
"status": "ok",
|
||||
"value": t("doctor_gpu_found", count=len(gpus), names=gpu_names),
|
||||
"hint": f"Total VRAM: {total_vram}GB",
|
||||
}
|
||||
)
|
||||
else:
|
||||
checks.append(
|
||||
{
|
||||
"name": t("doctor_check_gpu"),
|
||||
"status": "warning",
|
||||
"value": t("doctor_gpu_not_found"),
|
||||
"hint": "GPU recommended for best performance",
|
||||
}
|
||||
)
|
||||
|
||||
# 4. CPU information
|
||||
cpu_info = detect_cpu_info()
|
||||
checks.append(
|
||||
{
|
||||
"name": t("doctor_check_cpu"),
|
||||
"status": "ok",
|
||||
"value": t("doctor_cpu_info", name=cpu_info.name, cores=cpu_info.cores, threads=cpu_info.threads),
|
||||
"hint": None,
|
||||
}
|
||||
)
|
||||
|
||||
# 5. CPU instruction sets (critical for kt-kernel)
|
||||
isa_list = cpu_info.instruction_sets
|
||||
# Check for recommended instruction sets
|
||||
recommended_isa = {"AVX2", "AVX512F", "AMX-INT8"}
|
||||
has_recommended = bool(set(isa_list) & recommended_isa)
|
||||
has_avx2 = "AVX2" in isa_list
|
||||
has_avx512 = any(isa.startswith("AVX512") for isa in isa_list)
|
||||
has_amx = any(isa.startswith("AMX") for isa in isa_list)
|
||||
|
||||
# Determine status and build display string
|
||||
if has_amx:
|
||||
isa_status = "ok"
|
||||
isa_hint = "AMX available - best performance for INT4/INT8"
|
||||
elif has_avx512:
|
||||
isa_status = "ok"
|
||||
isa_hint = "AVX512 available - good performance"
|
||||
elif has_avx2:
|
||||
isa_status = "warning"
|
||||
isa_hint = "AVX2 only - consider upgrading CPU for better performance"
|
||||
else:
|
||||
isa_status = "error"
|
||||
isa_hint = "AVX2 required for kt-kernel"
|
||||
|
||||
# Show top instruction sets (prioritize important ones)
|
||||
display_isa = isa_list[:8] if len(isa_list) > 8 else isa_list
|
||||
isa_display = ", ".join(display_isa)
|
||||
if len(isa_list) > 8:
|
||||
isa_display += f" (+{len(isa_list) - 8} more)"
|
||||
|
||||
checks.append(
|
||||
{
|
||||
"name": t("doctor_check_cpu_isa"),
|
||||
"status": isa_status,
|
||||
"value": isa_display if isa_display else "None detected",
|
||||
"hint": isa_hint,
|
||||
}
|
||||
)
|
||||
|
||||
# 6. NUMA topology
|
||||
numa_detail = []
|
||||
for node, cpus in sorted(cpu_info.numa_info.items()):
|
||||
if len(cpus) > 6:
|
||||
cpu_str = f"{cpus[0]}-{cpus[-1]}"
|
||||
else:
|
||||
cpu_str = ",".join(str(c) for c in cpus)
|
||||
numa_detail.append(f"{node}: {cpu_str}")
|
||||
|
||||
numa_value = t("doctor_numa_info", nodes=cpu_info.numa_nodes)
|
||||
if verbose and numa_detail:
|
||||
numa_value += " (" + "; ".join(numa_detail) + ")"
|
||||
|
||||
checks.append(
|
||||
{
|
||||
"name": t("doctor_check_numa"),
|
||||
"status": "ok",
|
||||
"value": numa_value,
|
||||
"hint": f"{cpu_info.threads // cpu_info.numa_nodes} threads per node" if cpu_info.numa_nodes > 1 else None,
|
||||
}
|
||||
)
|
||||
|
||||
# 6b. kt-kernel installation check
|
||||
kt_info = _get_kt_kernel_info()
|
||||
|
||||
if kt_info["installed"]:
|
||||
# Build display string for kt-kernel
|
||||
variant = kt_info["cpu_variant"]
|
||||
version = kt_info["version"]
|
||||
available_variants = kt_info["available_variants"]
|
||||
|
||||
# Determine status based on CPU variant
|
||||
if variant == "amx":
|
||||
kt_status = "ok"
|
||||
kt_hint = "AMX variant loaded - optimal performance"
|
||||
elif variant.startswith("avx512"):
|
||||
kt_status = "ok"
|
||||
kt_hint = "AVX512 variant loaded - good performance"
|
||||
elif variant == "avx2":
|
||||
kt_status = "warning"
|
||||
kt_hint = "AVX2 variant - consider upgrading CPU for AMX/AVX512"
|
||||
else:
|
||||
kt_status = "warning"
|
||||
kt_hint = f"Unknown variant: {variant}"
|
||||
|
||||
kt_value = f"v{version} ({variant.upper()})"
|
||||
if verbose and available_variants:
|
||||
kt_value += f" [dim] - available: {', '.join(available_variants)}[/dim]"
|
||||
|
||||
checks.append(
|
||||
{
|
||||
"name": "kt-kernel",
|
||||
"status": kt_status,
|
||||
"value": kt_value,
|
||||
"hint": kt_hint,
|
||||
}
|
||||
)
|
||||
|
||||
# Show extension file path in verbose mode
|
||||
if verbose and kt_info.get("extension_file"):
|
||||
ext_file = os.path.basename(kt_info["extension_file"])
|
||||
checks.append(
|
||||
{
|
||||
"name": " └─ Extension",
|
||||
"status": "ok",
|
||||
"value": ext_file,
|
||||
"hint": None,
|
||||
}
|
||||
)
|
||||
|
||||
# Show installation path in verbose mode
|
||||
if verbose and kt_info.get("install_path"):
|
||||
checks.append(
|
||||
{
|
||||
"name": " └─ Path",
|
||||
"status": "ok",
|
||||
"value": kt_info["install_path"],
|
||||
"hint": None,
|
||||
}
|
||||
)
|
||||
else:
|
||||
error_msg = kt_info.get("error", "Not installed")
|
||||
checks.append(
|
||||
{
|
||||
"name": "kt-kernel",
|
||||
"status": "error",
|
||||
"value": error_msg,
|
||||
"hint": "kt-kernel is required - run: pip install kt-kernel",
|
||||
}
|
||||
)
|
||||
issues_found = True
|
||||
|
||||
# 7. System memory (with frequency if available)
|
||||
mem_info = detect_memory_info()
|
||||
if mem_info.frequency_mhz and mem_info.type:
|
||||
mem_value = t(
|
||||
"doctor_memory_freq",
|
||||
available=f"{mem_info.available_gb}GB",
|
||||
total=f"{mem_info.total_gb}GB",
|
||||
freq=mem_info.frequency_mhz,
|
||||
type=mem_info.type,
|
||||
)
|
||||
else:
|
||||
mem_value = t("doctor_memory_info", available=f"{mem_info.available_gb}GB", total=f"{mem_info.total_gb}GB")
|
||||
|
||||
ram_ok = mem_info.total_gb >= 32
|
||||
checks.append(
|
||||
{
|
||||
"name": t("doctor_check_memory"),
|
||||
"status": "ok" if ram_ok else "warning",
|
||||
"value": mem_value,
|
||||
"hint": "32GB+ RAM recommended for large models" if not ram_ok else None,
|
||||
}
|
||||
)
|
||||
|
||||
# 8. Disk space - check all model paths
|
||||
settings = get_settings()
|
||||
model_paths = settings.get_model_paths()
|
||||
|
||||
# Check all configured model paths
|
||||
for i, disk_path in enumerate(model_paths):
|
||||
available_disk, total_disk = detect_disk_space_gb(str(disk_path))
|
||||
disk_ok = available_disk >= 100
|
||||
|
||||
# For multiple paths, add index to name
|
||||
path_label = f"Model Path {i+1}" if len(model_paths) > 1 else t("doctor_check_disk")
|
||||
|
||||
checks.append(
|
||||
{
|
||||
"name": path_label,
|
||||
"status": "ok" if disk_ok else "warning",
|
||||
"value": t("doctor_disk_info", available=f"{available_disk}GB", path=str(disk_path)),
|
||||
"hint": "100GB+ free space recommended for model storage" if not disk_ok else None,
|
||||
}
|
||||
)
|
||||
|
||||
# 6. Required packages
|
||||
packages = [
|
||||
("kt-kernel", ">=0.4.0", False), # name, version_req, required
|
||||
("sglang", ">=0.4.0", False),
|
||||
("torch", ">=2.4.0", True),
|
||||
("transformers", ">=4.45.0", True),
|
||||
]
|
||||
|
||||
package_issues = []
|
||||
for pkg_name, version_req, required in packages:
|
||||
version = get_installed_package_version(pkg_name)
|
||||
if version:
|
||||
package_issues.append((pkg_name, version, "ok"))
|
||||
elif required:
|
||||
package_issues.append((pkg_name, t("version_not_installed"), "error"))
|
||||
issues_found = True
|
||||
else:
|
||||
package_issues.append((pkg_name, t("version_not_installed"), "warning"))
|
||||
|
||||
if verbose:
|
||||
checks.append(
|
||||
{
|
||||
"name": t("doctor_check_packages"),
|
||||
"status": "ok" if not any(p[2] == "error" for p in package_issues) else "error",
|
||||
"value": f"{sum(1 for p in package_issues if p[2] == 'ok')}/{len(package_issues)} installed",
|
||||
"packages": package_issues,
|
||||
}
|
||||
)
|
||||
|
||||
# 7. SGLang installation source check
|
||||
from kt_kernel.cli.utils.sglang_checker import check_sglang_installation, check_sglang_kt_kernel_support
|
||||
|
||||
sglang_info = check_sglang_installation()
|
||||
|
||||
if sglang_info["installed"]:
|
||||
if sglang_info.get("is_kvcache_fork"):
|
||||
# Package name is sglang-kt — this is definitively the kvcache-ai fork
|
||||
if sglang_info["from_source"] and sglang_info["git_info"]:
|
||||
git_remote = sglang_info["git_info"].get("remote", "unknown")
|
||||
git_branch = sglang_info["git_info"].get("branch", "unknown")
|
||||
sglang_source_value = f"sglang-kt (Source: {git_remote}, branch: {git_branch})"
|
||||
elif sglang_info["editable"]:
|
||||
sglang_source_value = "sglang-kt (editable)"
|
||||
else:
|
||||
sglang_source_value = "sglang-kt"
|
||||
sglang_source_status = "ok"
|
||||
sglang_source_hint = None
|
||||
elif sglang_info["from_source"]:
|
||||
if sglang_info["git_info"]:
|
||||
git_remote = sglang_info["git_info"].get("remote", "unknown")
|
||||
git_branch = sglang_info["git_info"].get("branch", "unknown")
|
||||
sglang_source_value = f"Source (GitHub: {git_remote}, branch: {git_branch})"
|
||||
sglang_source_status = "ok"
|
||||
sglang_source_hint = None
|
||||
else:
|
||||
sglang_source_value = "Source (editable)"
|
||||
sglang_source_status = "ok"
|
||||
sglang_source_hint = None
|
||||
else:
|
||||
sglang_source_value = "PyPI sglang (not kvcache-ai fork)"
|
||||
sglang_source_status = "warning"
|
||||
sglang_source_hint = t("sglang_pypi_hint")
|
||||
else:
|
||||
sglang_source_value = "Not installed"
|
||||
sglang_source_status = "warning"
|
||||
sglang_source_hint = t("sglang_install_hint")
|
||||
|
||||
checks.append(
|
||||
{
|
||||
"name": "SGLang Source",
|
||||
"status": sglang_source_status,
|
||||
"value": sglang_source_value,
|
||||
"hint": sglang_source_hint,
|
||||
}
|
||||
)
|
||||
|
||||
# 7b. SGLang kt-kernel support check (only if SGLang is installed)
|
||||
kt_kernel_support = {"supported": True} # Default to True if not checked
|
||||
if sglang_info["installed"]:
|
||||
# Use cache=False to force re-check in doctor, but silent=True since we show in table
|
||||
kt_kernel_support = check_sglang_kt_kernel_support(use_cache=False, silent=True)
|
||||
|
||||
if kt_kernel_support["supported"]:
|
||||
kt_kernel_value = t("sglang_kt_kernel_supported")
|
||||
kt_kernel_status = "ok"
|
||||
kt_kernel_hint = None
|
||||
else:
|
||||
kt_kernel_value = t("sglang_kt_kernel_not_supported")
|
||||
kt_kernel_status = "error"
|
||||
kt_kernel_hint = "Reinstall SGLang: pip uninstall sglang -y && pip install sglang-kt (or run ./install.sh from ktransformers root)"
|
||||
issues_found = True
|
||||
|
||||
checks.append(
|
||||
{
|
||||
"name": "SGLang kt-kernel",
|
||||
"status": kt_kernel_status,
|
||||
"value": kt_kernel_value,
|
||||
"hint": kt_kernel_hint,
|
||||
}
|
||||
)
|
||||
|
||||
# 8. Potentially conflicting environment variables
|
||||
# Only surface a row when the variable is actually present; no noise otherwise.
|
||||
dsv4_submode = os.environ.get("SGLANG_DSV4_2604_SUBMODE")
|
||||
if dsv4_submode:
|
||||
checks.append(
|
||||
{
|
||||
"name": "Env: SGLANG_DSV4_2604_SUBMODE",
|
||||
"status": "warning" if dsv4_submode == "2604B" else "ok",
|
||||
"value": dsv4_submode,
|
||||
"hint": (
|
||||
"Intended for MXFP4 launches only. "
|
||||
"Causes a startup crash when kt-method is not MXFP4. Unset it if unused."
|
||||
if dsv4_submode == "2604B"
|
||||
else None
|
||||
),
|
||||
}
|
||||
)
|
||||
|
||||
# 9. Environment managers
|
||||
env_managers = detect_env_managers()
|
||||
docker = check_docker()
|
||||
env_list = [f"{m.name} {m.version}" for m in env_managers]
|
||||
if docker:
|
||||
env_list.append(f"docker {docker.version}")
|
||||
|
||||
checks.append(
|
||||
{
|
||||
"name": "Environment Managers",
|
||||
"status": "ok" if env_list else "warning",
|
||||
"value": ", ".join(env_list) if env_list else "None found",
|
||||
"hint": "conda or docker recommended for installation" if not env_list else None,
|
||||
}
|
||||
)
|
||||
|
||||
# Display results
|
||||
_display_results(checks, verbose)
|
||||
|
||||
# Show SGLang installation instructions if not installed
|
||||
if not sglang_info["installed"]:
|
||||
from kt_kernel.cli.utils.sglang_checker import print_sglang_install_instructions
|
||||
|
||||
console.print()
|
||||
print_sglang_install_instructions()
|
||||
# Show kt-kernel installation instructions if SGLang is installed but doesn't support kt-kernel
|
||||
elif sglang_info["installed"] and not kt_kernel_support.get("supported", True):
|
||||
from kt_kernel.cli.utils.sglang_checker import print_sglang_kt_kernel_instructions
|
||||
|
||||
console.print()
|
||||
print_sglang_kt_kernel_instructions()
|
||||
|
||||
# Summary
|
||||
console.print()
|
||||
if issues_found:
|
||||
print_warning(t("doctor_has_issues"))
|
||||
else:
|
||||
print_success(t("doctor_all_ok"))
|
||||
console.print()
|
||||
|
||||
|
||||
def _check_python_version(version: str) -> bool:
|
||||
"""Check if Python version meets requirements."""
|
||||
parts = version.split(".")
|
||||
try:
|
||||
major, minor = int(parts[0]), int(parts[1])
|
||||
return major >= 3 and minor >= 10
|
||||
except (IndexError, ValueError):
|
||||
return False
|
||||
|
||||
|
||||
def _display_results(checks: list[dict], verbose: bool) -> None:
|
||||
"""Display diagnostic results."""
|
||||
table = Table(show_header=True, header_style="bold")
|
||||
table.add_column("Check", style="bold")
|
||||
table.add_column("Status", width=8)
|
||||
table.add_column("Value")
|
||||
if verbose:
|
||||
table.add_column("Notes", style="dim")
|
||||
|
||||
for check in checks:
|
||||
status = check["status"]
|
||||
if status == "ok":
|
||||
status_str = f"[green]{t('doctor_status_ok')}[/green]"
|
||||
elif status == "warning":
|
||||
status_str = f"[yellow]{t('doctor_status_warning')}[/yellow]"
|
||||
else:
|
||||
status_str = f"[red]{t('doctor_status_error')}[/red]"
|
||||
|
||||
if verbose:
|
||||
table.add_row(
|
||||
check["name"],
|
||||
status_str,
|
||||
check["value"],
|
||||
check.get("hint", ""),
|
||||
)
|
||||
else:
|
||||
table.add_row(
|
||||
check["name"],
|
||||
status_str,
|
||||
check["value"],
|
||||
)
|
||||
|
||||
# Show package details if verbose
|
||||
if verbose and "packages" in check:
|
||||
for pkg_name, pkg_version, pkg_status in check["packages"]:
|
||||
if pkg_status == "ok":
|
||||
pkg_status_str = "[green]✓[/green]"
|
||||
elif pkg_status == "warning":
|
||||
pkg_status_str = "[yellow]○[/yellow]"
|
||||
else:
|
||||
pkg_status_str = "[red]✗[/red]"
|
||||
|
||||
table.add_row(
|
||||
f" └─ {pkg_name}",
|
||||
pkg_status_str,
|
||||
pkg_version,
|
||||
"",
|
||||
)
|
||||
|
||||
console.print(table)
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,530 @@
|
||||
"""
|
||||
Quant command for kt-cli.
|
||||
|
||||
Quantizes model weights for CPU inference.
|
||||
"""
|
||||
|
||||
import subprocess
|
||||
import sys
|
||||
from enum import Enum
|
||||
from pathlib import Path
|
||||
from typing import Optional
|
||||
|
||||
import typer
|
||||
|
||||
from kt_kernel.cli.config.settings import get_settings
|
||||
from kt_kernel.cli.i18n import t
|
||||
from kt_kernel.cli.utils.console import (
|
||||
confirm,
|
||||
console,
|
||||
create_progress,
|
||||
print_error,
|
||||
print_info,
|
||||
print_step,
|
||||
print_success,
|
||||
print_warning,
|
||||
)
|
||||
from kt_kernel.cli.utils.environment import detect_cpu_info
|
||||
|
||||
|
||||
class QuantMethod(str, Enum):
|
||||
"""Quantization method."""
|
||||
|
||||
INT4 = "int4"
|
||||
INT8 = "int8"
|
||||
|
||||
|
||||
def quant(
|
||||
model: Optional[str] = typer.Argument(
|
||||
None,
|
||||
help="Model name or path to quantize",
|
||||
),
|
||||
method: Optional[QuantMethod] = typer.Option(
|
||||
None,
|
||||
"--method",
|
||||
"-m",
|
||||
help="Quantization method",
|
||||
),
|
||||
output: Optional[Path] = typer.Option(
|
||||
None,
|
||||
"--output",
|
||||
"-o",
|
||||
help="Output path for quantized weights",
|
||||
),
|
||||
input_type: Optional[str] = typer.Option(
|
||||
None,
|
||||
"--input-type",
|
||||
"-i",
|
||||
help="Input weight type (fp8, fp16, bf16)",
|
||||
),
|
||||
cpu_threads: Optional[int] = typer.Option(
|
||||
None,
|
||||
"--cpu-threads",
|
||||
help="Number of CPU threads for quantization",
|
||||
),
|
||||
numa_nodes: Optional[int] = typer.Option(
|
||||
None,
|
||||
"--numa-nodes",
|
||||
help="Number of NUMA nodes",
|
||||
),
|
||||
no_merge: bool = typer.Option(
|
||||
False,
|
||||
"--no-merge",
|
||||
help="Don't merge safetensor files",
|
||||
),
|
||||
gpu: bool = typer.Option(
|
||||
False,
|
||||
"--gpu",
|
||||
help="Use GPU for conversion (faster)",
|
||||
),
|
||||
yes: bool = typer.Option(
|
||||
False,
|
||||
"--yes",
|
||||
"-y",
|
||||
help="Skip confirmation prompts",
|
||||
),
|
||||
) -> None:
|
||||
"""Quantize model weights for CPU inference.
|
||||
|
||||
If no model is specified, interactive mode will be activated.
|
||||
"""
|
||||
settings = get_settings()
|
||||
|
||||
# Check if we should use interactive mode
|
||||
# Interactive mode triggers when: no model, or missing critical parameters
|
||||
needs_interactive = model is None or method is None or cpu_threads is None or numa_nodes is None
|
||||
is_interactive = False
|
||||
|
||||
if needs_interactive and sys.stdin.isatty():
|
||||
# Use interactive configuration (includes verification in Step 1.5)
|
||||
from kt_kernel.cli.utils.quant_interactive import interactive_quant_config
|
||||
|
||||
console.print()
|
||||
console.print(f"[bold cyan]═══ {t('quant_interactive_title')} ═══[/bold cyan]")
|
||||
console.print()
|
||||
console.print(f"[yellow]{t('quant_new_model_notice')}[/yellow]")
|
||||
console.print()
|
||||
|
||||
config = interactive_quant_config()
|
||||
if config is None:
|
||||
# User cancelled
|
||||
raise typer.Exit(0)
|
||||
|
||||
# Extract configuration
|
||||
model_obj = config["model"]
|
||||
model = model_obj.id
|
||||
input_path = Path(model_obj.path)
|
||||
method = QuantMethod(config["method"])
|
||||
input_type = config["input_type"]
|
||||
cpu_threads = config["cpu_threads"]
|
||||
numa_nodes = config["numa_nodes"]
|
||||
output = config["output_path"]
|
||||
gpu = config["use_gpu"]
|
||||
is_interactive = True
|
||||
|
||||
console.print()
|
||||
print_success(t("quant_config_complete"))
|
||||
console.print()
|
||||
else:
|
||||
# Non-interactive mode - require model parameter
|
||||
if model is None:
|
||||
print_error("Model argument is required in non-interactive mode")
|
||||
console.print()
|
||||
console.print("Usage: kt quant <model>")
|
||||
console.print(" Or: kt quant (for interactive mode)")
|
||||
raise typer.Exit(1)
|
||||
|
||||
# Set defaults for optional parameters
|
||||
method = method or QuantMethod.INT4
|
||||
input_type = input_type or "fp8"
|
||||
|
||||
console.print()
|
||||
|
||||
# Resolve input path
|
||||
input_path = _resolve_input_path(model, settings)
|
||||
if input_path is None:
|
||||
print_error(t("quant_input_not_found", path=model))
|
||||
raise typer.Exit(1)
|
||||
|
||||
# Pre-quantization verification (only in non-interactive mode)
|
||||
# Interactive mode already did verification in interactive_quant_config()
|
||||
from kt_kernel.cli.utils.user_model_registry import UserModelRegistry
|
||||
from kt_kernel.cli.utils.model_verifier import pre_operation_verification
|
||||
|
||||
user_registry = UserModelRegistry()
|
||||
user_model_obj = user_registry.find_by_path(str(input_path))
|
||||
|
||||
if user_model_obj and user_model_obj.format == "safetensors":
|
||||
pre_operation_verification(user_model_obj, user_registry, operation_name="quantizing")
|
||||
|
||||
# Get user model info for both modes (needed later for registering quantized model)
|
||||
from kt_kernel.cli.utils.user_model_registry import UserModelRegistry
|
||||
|
||||
user_registry = UserModelRegistry()
|
||||
user_model_obj = user_registry.find_by_path(str(input_path))
|
||||
|
||||
# Validate that it's a MoE model (not AMX or GGUF)
|
||||
from kt_kernel.cli.commands.model import is_amx_weights
|
||||
|
||||
# Check if it's AMX (already quantized)
|
||||
is_amx, _ = is_amx_weights(str(input_path))
|
||||
if is_amx:
|
||||
print_error("Cannot quantize AMX models (already quantized)")
|
||||
console.print()
|
||||
console.print(f" The model at {input_path} is already in AMX format.")
|
||||
raise typer.Exit(1)
|
||||
|
||||
# Check if it's a MoE model
|
||||
from kt_kernel.cli.utils.analyze_moe_model import analyze_moe_model
|
||||
|
||||
moe_result = None # Store for later use when registering quantized model
|
||||
try:
|
||||
moe_result = analyze_moe_model(str(input_path), use_cache=True)
|
||||
if not moe_result or not moe_result.get("is_moe"):
|
||||
print_error("Only MoE models can be quantized to AMX format")
|
||||
console.print()
|
||||
console.print(f" The model at {input_path} is not a MoE model.")
|
||||
console.print(" AMX quantization is designed for MoE models (e.g., DeepSeek-V3).")
|
||||
raise typer.Exit(1)
|
||||
except Exception as e:
|
||||
print_warning(f"Could not detect MoE information: {e}")
|
||||
console.print()
|
||||
if not yes:
|
||||
if not confirm("Continue quantization anyway?", default=False):
|
||||
raise typer.Exit(1)
|
||||
|
||||
# Detect CPU configuration and resolve output path (only needed in non-interactive mode)
|
||||
if not is_interactive:
|
||||
print_info(t("quant_input_path", path=str(input_path)))
|
||||
|
||||
# Detect CPU configuration (needed for output path)
|
||||
cpu = detect_cpu_info()
|
||||
final_cpu_threads = cpu_threads or cpu.cores
|
||||
final_numa_nodes = numa_nodes or cpu.numa_nodes
|
||||
|
||||
# Resolve output path
|
||||
if output is None:
|
||||
# Priority: paths.weights > paths.models[0] > model's parent directory
|
||||
weights_dir = settings.weights_dir
|
||||
|
||||
if weights_dir and weights_dir.exists():
|
||||
# Use configured weights directory (highest priority)
|
||||
output = weights_dir / f"{input_path.name}-AMX{method.value.upper()}-NUMA{final_numa_nodes}"
|
||||
else:
|
||||
# Use first model storage path
|
||||
model_paths = settings.get_model_paths()
|
||||
if model_paths and model_paths[0].exists():
|
||||
output = model_paths[0] / f"{input_path.name}-AMX{method.value.upper()}-NUMA{final_numa_nodes}"
|
||||
else:
|
||||
# Fallback to model's parent directory
|
||||
output = input_path.parent / f"{input_path.name}-AMX{method.value.upper()}-NUMA{final_numa_nodes}"
|
||||
|
||||
print_info(t("quant_output_path", path=str(output)))
|
||||
print_info(t("quant_method", method=method.value.upper()))
|
||||
print_info(t("quant_cpu_threads", threads=final_cpu_threads))
|
||||
print_info(t("quant_numa_nodes", nodes=final_numa_nodes))
|
||||
|
||||
# Calculate space requirements
|
||||
console.print()
|
||||
console.print(f"[bold cyan]{t('quant_disk_analysis')}[/bold cyan]")
|
||||
console.print()
|
||||
|
||||
# Calculate source model size
|
||||
try:
|
||||
total_bytes = sum(f.stat().st_size for f in input_path.glob("*.safetensors") if f.is_file())
|
||||
source_size_gb = total_bytes / (1024**3)
|
||||
except Exception:
|
||||
source_size_gb = 0.0
|
||||
|
||||
# Estimate quantized size
|
||||
input_bits = {"fp8": 8, "fp16": 16, "bf16": 16}
|
||||
quant_bits = {"int4": 4, "int8": 8}
|
||||
input_bit = input_bits.get(input_type, 16)
|
||||
quant_bit = quant_bits.get(method.value, 4)
|
||||
ratio = quant_bit / input_bit
|
||||
estimated_size_gb = source_size_gb * ratio
|
||||
|
||||
# Check available space
|
||||
import shutil
|
||||
|
||||
try:
|
||||
check_path = output.parent if not output.exists() else output
|
||||
while not check_path.exists() and check_path != check_path.parent:
|
||||
check_path = check_path.parent
|
||||
stat = shutil.disk_usage(check_path)
|
||||
available_gb = stat.free / (1024**3)
|
||||
except Exception:
|
||||
available_gb = 0.0
|
||||
|
||||
is_sufficient = available_gb >= (estimated_size_gb * 1.2)
|
||||
|
||||
console.print(f" {t('quant_source_size'):<26} {source_size_gb:.2f} GB")
|
||||
console.print(f" {t('quant_estimated_size'):<26} {estimated_size_gb:.2f} GB")
|
||||
console.print(f" {t('quant_available_space'):<26} {available_gb:.2f} GB")
|
||||
console.print()
|
||||
|
||||
if not is_sufficient:
|
||||
required_with_buffer = estimated_size_gb * 1.2
|
||||
print_warning(t("quant_insufficient_space"))
|
||||
console.print()
|
||||
console.print(f" {t('quant_required_space'):<26} {required_with_buffer:.2f} GB")
|
||||
console.print(f" {t('quant_available_space'):<26} {available_gb:.2f} GB")
|
||||
console.print(f" {t('quant_shortage'):<26} {required_with_buffer - available_gb:.2f} GB")
|
||||
console.print()
|
||||
console.print(f" {t('quant_may_fail')}")
|
||||
console.print()
|
||||
|
||||
if not yes:
|
||||
if not confirm(t("quant_continue_anyway"), default=False):
|
||||
raise typer.Abort()
|
||||
console.print()
|
||||
|
||||
# Check if output exists and generate unique name
|
||||
if output.exists():
|
||||
print_warning(t("quant_output_exists", path=str(output)))
|
||||
console.print()
|
||||
|
||||
# Generate unique name by adding suffix
|
||||
original_name = output.name
|
||||
parent_dir = output.parent
|
||||
counter = 2
|
||||
|
||||
while output.exists():
|
||||
new_name = f"{original_name}-{counter}"
|
||||
output = parent_dir / new_name
|
||||
counter += 1
|
||||
|
||||
print_success(t("quant_using_unique", path=str(output)))
|
||||
console.print()
|
||||
|
||||
# Confirm (only show if not using --yes flag)
|
||||
if not yes:
|
||||
console.print()
|
||||
print_warning(t("quant_time_warning"))
|
||||
console.print()
|
||||
|
||||
if not confirm(t("prompt_continue")):
|
||||
raise typer.Abort()
|
||||
else:
|
||||
# Interactive mode: cpu_threads and numa_nodes already set
|
||||
final_cpu_threads = cpu_threads
|
||||
final_numa_nodes = numa_nodes
|
||||
|
||||
# Find conversion script
|
||||
kt_kernel_path = _find_kt_kernel_path()
|
||||
if kt_kernel_path is None:
|
||||
print_error("kt-kernel not found. Install with: kt install inference")
|
||||
raise typer.Exit(1)
|
||||
|
||||
script_path = kt_kernel_path / "scripts" / "convert_cpu_weights.py"
|
||||
if not script_path.exists():
|
||||
print_error(f"Conversion script not found: {script_path}")
|
||||
raise typer.Exit(1)
|
||||
|
||||
# Build command
|
||||
cmd = [
|
||||
sys.executable,
|
||||
str(script_path),
|
||||
"--input-path",
|
||||
str(input_path),
|
||||
"--input-type",
|
||||
input_type,
|
||||
"--output",
|
||||
str(output),
|
||||
"--quant-method",
|
||||
method.value,
|
||||
"--cpuinfer-threads",
|
||||
str(final_cpu_threads),
|
||||
"--threadpool-count",
|
||||
str(final_numa_nodes),
|
||||
]
|
||||
|
||||
if no_merge:
|
||||
cmd.append("--no-merge-safetensor")
|
||||
|
||||
if gpu:
|
||||
cmd.append("--gpu")
|
||||
|
||||
# Run quantization
|
||||
console.print()
|
||||
print_step(t("quant_starting"))
|
||||
console.print()
|
||||
console.print(f"[dim]$ {' '.join(cmd)}[/dim]")
|
||||
console.print()
|
||||
console.print("[dim]" + "=" * 80 + "[/dim]")
|
||||
console.print()
|
||||
|
||||
try:
|
||||
# Run with real-time stdout/stderr output
|
||||
import os
|
||||
import time
|
||||
|
||||
env = os.environ.copy()
|
||||
env["PYTHONUNBUFFERED"] = "1" # Disable Python output buffering
|
||||
|
||||
# Record start time
|
||||
start_time = time.time()
|
||||
|
||||
process = subprocess.run(
|
||||
cmd,
|
||||
stdout=None, # Inherit parent's stdout (real-time output)
|
||||
stderr=None, # Inherit parent's stderr (real-time output)
|
||||
env=env,
|
||||
)
|
||||
|
||||
# Calculate elapsed time
|
||||
elapsed_time = time.time() - start_time
|
||||
hours = int(elapsed_time // 3600)
|
||||
minutes = int((elapsed_time % 3600) // 60)
|
||||
seconds = int(elapsed_time % 60)
|
||||
|
||||
console.print()
|
||||
console.print("[dim]" + "=" * 80 + "[/dim]")
|
||||
console.print()
|
||||
|
||||
if process.returncode == 0:
|
||||
print_success(t("quant_complete"))
|
||||
console.print()
|
||||
|
||||
# Display elapsed time
|
||||
if hours > 0:
|
||||
time_str = f"{hours}h {minutes}m {seconds}s"
|
||||
elif minutes > 0:
|
||||
time_str = f"{minutes}m {seconds}s"
|
||||
else:
|
||||
time_str = f"{seconds}s"
|
||||
console.print(f" [cyan]{t('quant_time_elapsed')} {time_str}[/cyan]")
|
||||
console.print()
|
||||
console.print(f" Quantized weights saved to: {output}")
|
||||
console.print()
|
||||
|
||||
# Auto-register the quantized model
|
||||
try:
|
||||
from kt_kernel.cli.utils.user_model_registry import UserModel
|
||||
|
||||
# Generate model name from output path
|
||||
base_name = output.name
|
||||
suggested_name = user_registry.suggest_name(base_name)
|
||||
|
||||
# Determine MoE information and source model name
|
||||
if user_model_obj:
|
||||
is_moe_val = user_model_obj.is_moe
|
||||
num_experts = user_model_obj.moe_num_experts
|
||||
num_active = user_model_obj.moe_num_experts_per_tok
|
||||
repo_type_val = user_model_obj.repo_type
|
||||
repo_id_val = user_model_obj.repo_id
|
||||
source_model_name = user_model_obj.name # Store source model name
|
||||
elif moe_result:
|
||||
is_moe_val = moe_result.get("is_moe", True)
|
||||
num_experts = moe_result.get("num_experts")
|
||||
num_active = moe_result.get("num_experts_per_tok")
|
||||
repo_type_val = None
|
||||
repo_id_val = None
|
||||
source_model_name = input_path.name # Use folder name as fallback
|
||||
else:
|
||||
is_moe_val = None
|
||||
num_experts = None
|
||||
num_active = None
|
||||
repo_type_val = None
|
||||
repo_id_val = None
|
||||
source_model_name = input_path.name # Use folder name as fallback
|
||||
|
||||
# Create new model entry (AMX format uses "safetensors" format, detected by is_amx_weights())
|
||||
new_model = UserModel(
|
||||
name=suggested_name,
|
||||
path=str(output),
|
||||
format="safetensors", # AMX files are safetensors format
|
||||
repo_type=repo_type_val,
|
||||
repo_id=repo_id_val,
|
||||
sha256_status="not_checked", # AMX weights don't need verification
|
||||
# Inherit MoE information from source model
|
||||
is_moe=is_moe_val,
|
||||
moe_num_experts=num_experts,
|
||||
moe_num_experts_per_tok=num_active,
|
||||
# AMX quantization metadata
|
||||
amx_source_model=source_model_name,
|
||||
amx_quant_method=method.value, # "int4" or "int8"
|
||||
amx_numa_nodes=final_numa_nodes,
|
||||
)
|
||||
|
||||
user_registry.add_model(new_model)
|
||||
console.print()
|
||||
print_success(t("quant_registered", name=suggested_name))
|
||||
console.print()
|
||||
console.print(f" {t('quant_view_with')} [cyan]kt model list[/cyan]")
|
||||
console.print(f" {t('quant_use_with')} [cyan]kt run {suggested_name}[/cyan]")
|
||||
console.print()
|
||||
except Exception as e:
|
||||
# Non-fatal error - quantization succeeded but registration failed
|
||||
console.print()
|
||||
print_warning(t("quant_register_failed", error=str(e)))
|
||||
console.print()
|
||||
console.print(f" {t('quant_use_with')}")
|
||||
console.print(f" kt run {model} --weights-path {output}")
|
||||
console.print()
|
||||
else:
|
||||
print_error(f"Quantization failed with exit code {process.returncode}")
|
||||
raise typer.Exit(process.returncode)
|
||||
|
||||
except FileNotFoundError as e:
|
||||
print_error(f"Failed to run quantization: {e}")
|
||||
raise typer.Exit(1)
|
||||
except KeyboardInterrupt:
|
||||
console.print()
|
||||
print_warning("Quantization interrupted.")
|
||||
raise typer.Exit(130)
|
||||
|
||||
|
||||
def _resolve_input_path(model: str, settings) -> Optional[Path]:
|
||||
"""Resolve the input model path."""
|
||||
# Check if it's already a path
|
||||
path = Path(model)
|
||||
if path.exists() and (path / "config.json").exists():
|
||||
return path
|
||||
|
||||
# Search in models directory
|
||||
from kt_kernel.cli.utils.model_registry import get_registry
|
||||
|
||||
registry = get_registry()
|
||||
matches = registry.search(model)
|
||||
|
||||
if matches:
|
||||
model_info = matches[0]
|
||||
# Try to find in all configured model directories
|
||||
model_paths = settings.get_model_paths()
|
||||
|
||||
for models_dir in model_paths:
|
||||
possible_paths = [
|
||||
models_dir / model_info.name,
|
||||
models_dir / model_info.name.lower(),
|
||||
models_dir / model_info.hf_repo.split("/")[-1],
|
||||
]
|
||||
|
||||
for p in possible_paths:
|
||||
if p.exists() and (p / "config.json").exists():
|
||||
return p
|
||||
|
||||
return None
|
||||
|
||||
|
||||
def _find_kt_kernel_path() -> Optional[Path]:
|
||||
"""Find the kt-kernel installation path."""
|
||||
try:
|
||||
import kt_kernel
|
||||
|
||||
return Path(kt_kernel.__file__).parent.parent
|
||||
except ImportError:
|
||||
pass
|
||||
|
||||
# Check common locations
|
||||
possible_paths = [
|
||||
Path.home() / "Projects" / "ktransformers" / "kt-kernel",
|
||||
Path.cwd().parent / "kt-kernel",
|
||||
Path.cwd() / "kt-kernel",
|
||||
]
|
||||
|
||||
for path in possible_paths:
|
||||
if path.exists() and (path / "scripts").exists():
|
||||
return path
|
||||
|
||||
return None
|
||||
@@ -0,0 +1,838 @@
|
||||
"""
|
||||
Run command for kt-cli.
|
||||
|
||||
Starts the model inference server using SGLang + kt-kernel.
|
||||
"""
|
||||
|
||||
import os
|
||||
import subprocess
|
||||
import sys
|
||||
from pathlib import Path
|
||||
from typing import Optional
|
||||
|
||||
import click
|
||||
import typer
|
||||
|
||||
from kt_kernel.cli.config.settings import get_settings
|
||||
from kt_kernel.cli.i18n import t
|
||||
from kt_kernel.cli.utils.console import (
|
||||
confirm,
|
||||
console,
|
||||
print_api_info,
|
||||
print_error,
|
||||
print_info,
|
||||
print_server_info,
|
||||
print_step,
|
||||
print_success,
|
||||
print_warning,
|
||||
prompt_choice,
|
||||
)
|
||||
from kt_kernel.cli.utils.environment import detect_cpu_info, detect_gpus, detect_ram_gb
|
||||
from kt_kernel.cli.utils.user_model_registry import UserModelRegistry
|
||||
|
||||
|
||||
@click.command(
|
||||
context_settings={"ignore_unknown_options": True, "allow_extra_args": True},
|
||||
add_help_option=False, # We'll handle help manually to avoid conflicts
|
||||
)
|
||||
@click.argument("model", required=False, default=None)
|
||||
@click.option("--host", "-H", default=None, help="Server host address")
|
||||
@click.option("--port", "-p", type=int, default=None, help="Server port")
|
||||
@click.option("--gpu-experts", type=int, default=None, help="Number of GPU experts per layer")
|
||||
@click.option("--cpu-threads", type=int, default=None, help="Number of CPU inference threads")
|
||||
@click.option(
|
||||
"--numa-nodes",
|
||||
"numa_nodes",
|
||||
type=int,
|
||||
multiple=True,
|
||||
default=(),
|
||||
help="Number of KT threadpools, or explicit NUMA node IDs for each threadpool (e.g. --numa-nodes 2 or --numa-nodes 0 --numa-nodes 1)",
|
||||
)
|
||||
@click.option(
|
||||
"--tensor-parallel-size", "--tp", "tensor_parallel_size", type=int, default=None, help="Tensor parallel size"
|
||||
)
|
||||
@click.option("--model-path", type=click.Path(), default=None, help="Custom model path")
|
||||
@click.option("--weights-path", type=click.Path(), default=None, help="Custom quantized weights path")
|
||||
@click.option("--kt-method", default=None, help="KT quantization method")
|
||||
@click.option(
|
||||
"--kt-gpu-prefill-threshold", "kt_gpu_prefill_threshold", type=int, default=None, help="GPU prefill token threshold"
|
||||
)
|
||||
@click.option("--attention-backend", default=None, help="Attention backend")
|
||||
@click.option("--max-total-tokens", "max_total_tokens", type=int, default=None, help="Maximum total tokens")
|
||||
@click.option("--max-running-requests", "max_running_requests", type=int, default=None, help="Maximum running requests")
|
||||
@click.option("--chunked-prefill-size", "chunked_prefill_size", type=int, default=None, help="Chunked prefill size")
|
||||
@click.option("--mem-fraction-static", "mem_fraction_static", type=float, default=None, help="Memory fraction static")
|
||||
@click.option("--watchdog-timeout", "watchdog_timeout", type=int, default=None, help="Watchdog timeout")
|
||||
@click.option("--served-model-name", "served_model_name", default=None, help="Served model name")
|
||||
@click.option(
|
||||
"--disable-shared-experts-fusion",
|
||||
"disable_shared_experts_fusion",
|
||||
is_flag=True,
|
||||
default=None,
|
||||
help="Disable shared experts fusion",
|
||||
)
|
||||
@click.option(
|
||||
"--enable-shared-experts-fusion",
|
||||
"enable_shared_experts_fusion",
|
||||
is_flag=True,
|
||||
default=False,
|
||||
help="Enable shared experts fusion",
|
||||
)
|
||||
@click.option("--quantize", "-q", is_flag=True, default=False, help="Quantize model")
|
||||
@click.option("--advanced", is_flag=True, default=False, help="Show advanced options")
|
||||
@click.option("--dry-run", "dry_run", is_flag=True, default=False, help="Show command without executing")
|
||||
@click.pass_context
|
||||
def run(
|
||||
ctx: click.Context,
|
||||
model: Optional[str],
|
||||
host: Optional[str],
|
||||
port: Optional[int],
|
||||
gpu_experts: Optional[int],
|
||||
cpu_threads: Optional[int],
|
||||
numa_nodes: Optional[tuple[int, ...]],
|
||||
tensor_parallel_size: Optional[int],
|
||||
model_path: Optional[str],
|
||||
weights_path: Optional[str],
|
||||
kt_method: Optional[str],
|
||||
kt_gpu_prefill_threshold: Optional[int],
|
||||
attention_backend: Optional[str],
|
||||
max_total_tokens: Optional[int],
|
||||
max_running_requests: Optional[int],
|
||||
chunked_prefill_size: Optional[int],
|
||||
mem_fraction_static: Optional[float],
|
||||
watchdog_timeout: Optional[int],
|
||||
served_model_name: Optional[str],
|
||||
disable_shared_experts_fusion: Optional[bool],
|
||||
enable_shared_experts_fusion: bool,
|
||||
quantize: bool,
|
||||
advanced: bool,
|
||||
dry_run: bool,
|
||||
) -> None:
|
||||
"""Start model inference server.
|
||||
|
||||
\b
|
||||
Examples: kt run deepseek-v3 | kt run m2 --tensor-parallel-size 2 | kt run /path/to/model --gpu-experts 4
|
||||
|
||||
\b
|
||||
Custom Options: Pass any SGLang server option directly (e.g., kt run m2 --fp8-gemm-backend triton).
|
||||
Common: --fp8-gemm-backend, --tool-call-parser, --reasoning-parser, --dp-size, --enable-ma
|
||||
For full list: python -m sglang.launch_server --help
|
||||
"""
|
||||
# Handle --help manually since we disabled it
|
||||
# Check sys.argv for --help or -h since ctx.args may not be set yet
|
||||
if "--help" in sys.argv or "-h" in sys.argv:
|
||||
click.echo(ctx.get_help())
|
||||
return
|
||||
|
||||
# Handle disable/enable shared experts fusion flags
|
||||
if enable_shared_experts_fusion:
|
||||
disable_shared_experts_fusion = False
|
||||
|
||||
# Convert Path objects from click
|
||||
model_path_obj = Path(model_path) if model_path else None
|
||||
weights_path_obj = Path(weights_path) if weights_path else None
|
||||
|
||||
# Get extra args that weren't parsed (unknown options)
|
||||
# click stores these in ctx.args when ignore_unknown_options=True
|
||||
extra_cli_args = list(ctx.args) if ctx.args else []
|
||||
|
||||
# Remove --help from extra args if present (already handled)
|
||||
extra_cli_args = [arg for arg in extra_cli_args if arg not in ["--help", "-h"]]
|
||||
|
||||
# Call the actual run function implementation
|
||||
_run_impl(
|
||||
model=model,
|
||||
host=host,
|
||||
port=port,
|
||||
gpu_experts=gpu_experts,
|
||||
cpu_threads=cpu_threads,
|
||||
numa_nodes=numa_nodes,
|
||||
tensor_parallel_size=tensor_parallel_size,
|
||||
model_path=model_path_obj,
|
||||
weights_path=weights_path_obj,
|
||||
kt_method=kt_method,
|
||||
kt_gpu_prefill_threshold=kt_gpu_prefill_threshold,
|
||||
attention_backend=attention_backend,
|
||||
max_total_tokens=max_total_tokens,
|
||||
max_running_requests=max_running_requests,
|
||||
chunked_prefill_size=chunked_prefill_size,
|
||||
mem_fraction_static=mem_fraction_static,
|
||||
watchdog_timeout=watchdog_timeout,
|
||||
served_model_name=served_model_name,
|
||||
disable_shared_experts_fusion=disable_shared_experts_fusion,
|
||||
quantize=quantize,
|
||||
advanced=advanced,
|
||||
dry_run=dry_run,
|
||||
extra_cli_args=extra_cli_args,
|
||||
)
|
||||
|
||||
|
||||
def _run_impl(
|
||||
model: Optional[str],
|
||||
host: Optional[str],
|
||||
port: Optional[int],
|
||||
gpu_experts: Optional[int],
|
||||
cpu_threads: Optional[int],
|
||||
numa_nodes: Optional[tuple[int, ...]],
|
||||
tensor_parallel_size: Optional[int],
|
||||
model_path: Optional[Path],
|
||||
weights_path: Optional[Path],
|
||||
kt_method: Optional[str],
|
||||
kt_gpu_prefill_threshold: Optional[int],
|
||||
attention_backend: Optional[str],
|
||||
max_total_tokens: Optional[int],
|
||||
max_running_requests: Optional[int],
|
||||
chunked_prefill_size: Optional[int],
|
||||
mem_fraction_static: Optional[float],
|
||||
watchdog_timeout: Optional[int],
|
||||
served_model_name: Optional[str],
|
||||
disable_shared_experts_fusion: Optional[bool],
|
||||
quantize: bool,
|
||||
advanced: bool,
|
||||
dry_run: bool,
|
||||
extra_cli_args: list[str],
|
||||
) -> None:
|
||||
"""Actual implementation of run command."""
|
||||
# Check if SGLang is installed before proceeding
|
||||
from kt_kernel.cli.utils.sglang_checker import (
|
||||
check_sglang_installation,
|
||||
check_sglang_kt_kernel_support,
|
||||
print_sglang_install_instructions,
|
||||
print_sglang_kt_kernel_instructions,
|
||||
)
|
||||
|
||||
sglang_info = check_sglang_installation()
|
||||
if not sglang_info["installed"]:
|
||||
console.print()
|
||||
print_error(t("sglang_not_found"))
|
||||
console.print()
|
||||
print_sglang_install_instructions()
|
||||
raise typer.Exit(1)
|
||||
|
||||
# Check if SGLang supports kt-kernel (has --kt-gpu-prefill-token-threshold parameter)
|
||||
kt_kernel_support = check_sglang_kt_kernel_support()
|
||||
if not kt_kernel_support["supported"]:
|
||||
console.print()
|
||||
print_error(t("sglang_kt_kernel_not_supported"))
|
||||
console.print()
|
||||
print_sglang_kt_kernel_instructions()
|
||||
raise typer.Exit(1)
|
||||
|
||||
settings = get_settings()
|
||||
user_registry = UserModelRegistry()
|
||||
|
||||
# Check if we should use interactive mode
|
||||
# Interactive mode triggers when:
|
||||
# 1. No model specified, OR
|
||||
# 2. Model specified but missing critical parameters (gpu_experts, tensor_parallel_size, etc.)
|
||||
use_interactive = False
|
||||
|
||||
if model is None:
|
||||
use_interactive = True
|
||||
elif (
|
||||
gpu_experts is None
|
||||
or tensor_parallel_size is None
|
||||
or cpu_threads is None
|
||||
or not numa_nodes
|
||||
or max_total_tokens is None
|
||||
):
|
||||
# Model specified but some parameters missing - use interactive
|
||||
use_interactive = True
|
||||
|
||||
if use_interactive and sys.stdin.isatty():
|
||||
# Use new interactive configuration flow
|
||||
from kt_kernel.cli.utils.run_interactive import interactive_run_config
|
||||
|
||||
console.print()
|
||||
console.print("[bold cyan]═══ Interactive Run Configuration ═══[/bold cyan]")
|
||||
console.print()
|
||||
|
||||
config = interactive_run_config()
|
||||
if config is None:
|
||||
# User cancelled
|
||||
raise typer.Exit(0)
|
||||
|
||||
# Extract configuration from new format
|
||||
user_model_obj = config["model"]
|
||||
model = user_model_obj.id
|
||||
resolved_model_path = Path(config["model_path"])
|
||||
resolved_weights_path = Path(config["weights_path"])
|
||||
|
||||
# Extract parameters
|
||||
gpu_experts = config["gpu_experts"]
|
||||
cpu_threads = config["cpu_threads"]
|
||||
if config.get("numa_nodes") is not None:
|
||||
numa_nodes = (int(config["numa_nodes"]),)
|
||||
else:
|
||||
numa_nodes = ()
|
||||
tensor_parallel_size = config["tp_size"]
|
||||
|
||||
# Get kt-method and other method-specific settings
|
||||
kt_method = config["kt_method"]
|
||||
|
||||
# KV cache settings (may be None for non-raw methods)
|
||||
max_total_tokens = config.get("kv_cache", 32768)
|
||||
chunked_prefill_size = config.get("chunk_prefill", 32768)
|
||||
kt_gpu_prefill_threshold = config.get("gpu_prefill_threshold", 500)
|
||||
|
||||
# Memory settings
|
||||
mem_fraction_static = config["mem_fraction_static"]
|
||||
|
||||
# Parser settings (optional)
|
||||
tool_call_parser = config.get("tool_call_parser")
|
||||
reasoning_parser = config.get("reasoning_parser")
|
||||
|
||||
# Server settings
|
||||
host = config.get("host", "0.0.0.0")
|
||||
port = config.get("port", 30000)
|
||||
|
||||
# Set CUDA_VISIBLE_DEVICES for selected GPUs
|
||||
selected_gpus = config["selected_gpus"]
|
||||
os.environ["CUDA_VISIBLE_DEVICES"] = ",".join(str(gpu_id) for gpu_id in selected_gpus)
|
||||
|
||||
# Detect hardware for parameter resolution (needed for resolve() function later)
|
||||
gpus = detect_gpus()
|
||||
cpu = detect_cpu_info()
|
||||
|
||||
console.print()
|
||||
print_info(f"[green]✓[/green] Configuration complete")
|
||||
console.print()
|
||||
else:
|
||||
# Non-interactive mode - use traditional flow
|
||||
console.print()
|
||||
|
||||
# Initialize variables that may have been set by interactive mode
|
||||
# These will be None in non-interactive mode and will use defaults via resolve()
|
||||
|
||||
# If no model specified, show old interactive selection
|
||||
if model is None:
|
||||
model = _interactive_model_selection(user_registry, settings)
|
||||
if model is None:
|
||||
raise typer.Exit(0)
|
||||
|
||||
# Detect hardware (needed for defaults)
|
||||
gpus = detect_gpus()
|
||||
cpu = detect_cpu_info()
|
||||
ram = detect_ram_gb()
|
||||
|
||||
if gpus:
|
||||
gpu_info = f"{gpus[0].name} ({gpus[0].vram_gb}GB VRAM)"
|
||||
if len(gpus) > 1:
|
||||
gpu_info += f" + {len(gpus) - 1} more"
|
||||
print_info(t("run_gpu_info", name=gpus[0].name, vram=gpus[0].vram_gb))
|
||||
else:
|
||||
print_warning(t("doctor_gpu_not_found"))
|
||||
gpu_info = "None"
|
||||
|
||||
print_info(t("run_cpu_info", name=cpu.name, cores=cpu.cores, numa=cpu.numa_nodes))
|
||||
print_info(t("run_ram_info", total=int(ram)))
|
||||
|
||||
# Step 2: Resolve model
|
||||
console.print()
|
||||
print_step(t("run_checking_model"))
|
||||
|
||||
user_model_obj = None
|
||||
resolved_model_path = model_path
|
||||
|
||||
# Check if model is a path
|
||||
if Path(model).exists():
|
||||
resolved_model_path = Path(model)
|
||||
print_info(t("run_model_path", path=str(resolved_model_path)))
|
||||
|
||||
# Try to find in user registry by path
|
||||
user_model_obj = user_registry.find_by_path(str(resolved_model_path))
|
||||
if user_model_obj:
|
||||
print_info(f"Using registered model: {user_model_obj.name}")
|
||||
else:
|
||||
print_warning("Using unregistered model path. Consider adding it with 'kt model add'")
|
||||
else:
|
||||
# Search in user registry by name
|
||||
user_model_obj = user_registry.get_model(model)
|
||||
|
||||
if not user_model_obj:
|
||||
print_error(t("run_model_not_found", name=model))
|
||||
console.print()
|
||||
|
||||
# Show available models
|
||||
all_models = user_registry.list_models()
|
||||
if all_models:
|
||||
console.print("Available registered models:")
|
||||
for m in all_models[:5]:
|
||||
console.print(f" - {m.name}")
|
||||
if len(all_models) > 5:
|
||||
console.print(f" ... and {len(all_models) - 5} more")
|
||||
else:
|
||||
console.print("No models registered yet.")
|
||||
|
||||
console.print()
|
||||
console.print(f"Add your model with: [cyan]kt model add /path/to/model[/cyan]")
|
||||
console.print(f"Or scan for models: [cyan]kt model scan[/cyan]")
|
||||
raise typer.Exit(1)
|
||||
|
||||
# Use model path from registry
|
||||
resolved_model_path = Path(user_model_obj.path)
|
||||
|
||||
# Verify path exists
|
||||
if not resolved_model_path.exists():
|
||||
print_error(f"Model path does not exist: {resolved_model_path}")
|
||||
console.print()
|
||||
console.print(f"Run 'kt model refresh' to check all models")
|
||||
raise typer.Exit(1)
|
||||
|
||||
print_info(t("run_model_path", path=str(resolved_model_path)))
|
||||
|
||||
# Step 2.5: Pre-run verification (optional integrity check)
|
||||
if user_model_obj and user_model_obj.format == "safetensors":
|
||||
from kt_kernel.cli.utils.model_verifier import pre_operation_verification
|
||||
|
||||
pre_operation_verification(user_model_obj, user_registry, operation_name="running")
|
||||
|
||||
# Step 3: Check quantized weights (only if explicitly requested)
|
||||
resolved_weights_path = None
|
||||
|
||||
# Only use quantized weights if explicitly specified by user
|
||||
if weights_path is not None:
|
||||
# User explicitly specified weights path
|
||||
resolved_weights_path = weights_path
|
||||
if not resolved_weights_path.exists():
|
||||
print_error(t("run_weights_not_found"))
|
||||
console.print(f" Path: {resolved_weights_path}")
|
||||
raise typer.Exit(1)
|
||||
print_info(f"Using quantized weights: {resolved_weights_path}")
|
||||
elif quantize:
|
||||
# User requested quantization
|
||||
console.print()
|
||||
print_step(t("run_quantizing"))
|
||||
# TODO: Implement quantization
|
||||
print_warning("Quantization not yet implemented. Please run 'kt quant' manually.")
|
||||
raise typer.Exit(1)
|
||||
else:
|
||||
# Default: use original precision model without quantization
|
||||
console.print()
|
||||
print_info("Using original precision model (no quantization)")
|
||||
|
||||
# Step 4: Build command
|
||||
# Helper to resolve parameter with fallback chain: CLI > config > default
|
||||
def resolve(cli_val, config_key, default):
|
||||
if cli_val is not None:
|
||||
return cli_val
|
||||
config_val = settings.get(config_key)
|
||||
return config_val if config_val is not None else default
|
||||
|
||||
# Server configuration
|
||||
final_host = resolve(host, "server.host", "0.0.0.0")
|
||||
final_port = resolve(port, "server.port", 30000)
|
||||
|
||||
# Tensor parallel size: CLI > config > auto-detect from GPUs
|
||||
final_tensor_parallel_size = resolve(
|
||||
tensor_parallel_size, "inference.tensor_parallel_size", len(gpus) if gpus else 1
|
||||
)
|
||||
|
||||
# CPU/GPU configuration with smart defaults
|
||||
total_threads = cpu.threads # Use logical threads instead of physical cores
|
||||
final_cpu_threads = resolve(cpu_threads, "inference.cpu_threads", int(total_threads * 0.8))
|
||||
final_numa_nodes = resolve(None, "inference.numa_nodes", cpu.numa_nodes)
|
||||
final_kt_numa_nodes = None
|
||||
|
||||
if numa_nodes:
|
||||
if len(numa_nodes) == 1:
|
||||
final_numa_nodes = numa_nodes[0]
|
||||
else:
|
||||
final_kt_numa_nodes = list(numa_nodes)
|
||||
final_numa_nodes = len(final_kt_numa_nodes)
|
||||
final_gpu_experts = resolve(gpu_experts, "inference.gpu_experts", 1)
|
||||
|
||||
# KT-kernel options
|
||||
final_kt_method = resolve(kt_method, "inference.kt_method", "AMXINT4")
|
||||
final_kt_gpu_prefill_threshold = resolve(kt_gpu_prefill_threshold, "inference.kt_gpu_prefill_token_threshold", 4096)
|
||||
|
||||
# SGLang options
|
||||
final_attention_backend = resolve(attention_backend, "inference.attention_backend", "flashinfer")
|
||||
final_max_total_tokens = resolve(max_total_tokens, "inference.max_total_tokens", 40000)
|
||||
final_max_running_requests = resolve(max_running_requests, "inference.max_running_requests", 32)
|
||||
final_chunked_prefill_size = resolve(chunked_prefill_size, "inference.chunked_prefill_size", 4096)
|
||||
final_mem_fraction_static = resolve(mem_fraction_static, "inference.mem_fraction_static", 0.98)
|
||||
final_watchdog_timeout = resolve(watchdog_timeout, "inference.watchdog_timeout", 3000)
|
||||
final_served_model_name = resolve(served_model_name, "inference.served_model_name", "")
|
||||
# Performance flags
|
||||
final_disable_shared_experts_fusion = resolve(
|
||||
disable_shared_experts_fusion, "inference.disable_shared_experts_fusion", True
|
||||
)
|
||||
|
||||
# Pass extra CLI parameters
|
||||
extra_params = {}
|
||||
|
||||
# Parser parameters (from interactive mode or None in non-interactive mode)
|
||||
final_tool_call_parser = None
|
||||
final_reasoning_parser = None
|
||||
if "tool_call_parser" in locals() and tool_call_parser:
|
||||
final_tool_call_parser = tool_call_parser
|
||||
if "reasoning_parser" in locals() and reasoning_parser:
|
||||
final_reasoning_parser = reasoning_parser
|
||||
|
||||
cmd = _build_sglang_command(
|
||||
model_path=resolved_model_path,
|
||||
weights_path=resolved_weights_path,
|
||||
host=final_host,
|
||||
port=final_port,
|
||||
gpu_experts=final_gpu_experts,
|
||||
cpu_threads=final_cpu_threads,
|
||||
numa_nodes=final_numa_nodes,
|
||||
tensor_parallel_size=final_tensor_parallel_size,
|
||||
kt_method=final_kt_method,
|
||||
kt_gpu_prefill_threshold=final_kt_gpu_prefill_threshold,
|
||||
attention_backend=final_attention_backend,
|
||||
max_total_tokens=final_max_total_tokens,
|
||||
max_running_requests=final_max_running_requests,
|
||||
chunked_prefill_size=final_chunked_prefill_size,
|
||||
mem_fraction_static=final_mem_fraction_static,
|
||||
watchdog_timeout=final_watchdog_timeout,
|
||||
served_model_name=final_served_model_name,
|
||||
disable_shared_experts_fusion=final_disable_shared_experts_fusion,
|
||||
kt_numa_nodes=final_kt_numa_nodes,
|
||||
tool_call_parser=final_tool_call_parser,
|
||||
reasoning_parser=final_reasoning_parser,
|
||||
settings=settings,
|
||||
extra_model_params=extra_params,
|
||||
extra_cli_args=extra_cli_args,
|
||||
)
|
||||
|
||||
# Prepare environment variables
|
||||
env = os.environ.copy()
|
||||
# Add environment variables from advanced.env
|
||||
env.update(settings.get_env_vars())
|
||||
# Add environment variables from inference.env
|
||||
inference_env = settings.get("inference.env", {})
|
||||
if isinstance(inference_env, dict):
|
||||
env.update({k: str(v) for k, v in inference_env.items()})
|
||||
|
||||
# Fail fast if a conflicting env var would crash sglang during model loading.
|
||||
# Check against the fully-assembled env dict (shell + kt config settings) so
|
||||
# nothing slips through regardless of where the variable was set.
|
||||
_check_conflicting_env_vars(final_kt_method, env)
|
||||
|
||||
# Step 5: Show configuration summary
|
||||
console.print()
|
||||
print_step("Configuration")
|
||||
|
||||
# Display model name
|
||||
model_display_name = user_model_obj.name if user_model_obj else resolved_model_path.name
|
||||
console.print(f" Model: [bold]{model_display_name}[/bold]")
|
||||
|
||||
console.print(f" Path: [dim]{resolved_model_path}[/dim]")
|
||||
|
||||
# Key parameters
|
||||
console.print()
|
||||
console.print(f" GPU Experts: [cyan]{final_gpu_experts}[/cyan] per layer")
|
||||
console.print(f" CPU Threads (kt-cpuinfer): [cyan]{final_cpu_threads}[/cyan]")
|
||||
console.print(f" NUMA Nodes (kt-threadpool-count): [cyan]{final_numa_nodes}[/cyan]")
|
||||
if final_kt_numa_nodes is not None:
|
||||
console.print(f" NUMA Nodes (binding): [cyan]{', '.join(map(str, final_kt_numa_nodes))}[/cyan]")
|
||||
console.print(f" Tensor Parallel: [cyan]{final_tensor_parallel_size}[/cyan]")
|
||||
console.print(f" Method: [cyan]{final_kt_method}[/cyan]")
|
||||
console.print(f" Attention: [cyan]{final_attention_backend}[/cyan]")
|
||||
|
||||
# Weights info
|
||||
if resolved_weights_path:
|
||||
console.print()
|
||||
console.print(f" Quantized weights: [yellow]{resolved_weights_path}[/yellow]")
|
||||
|
||||
console.print()
|
||||
console.print(f" Server: [green]http://{final_host}:{final_port}[/green]")
|
||||
console.print()
|
||||
|
||||
# Step 6: Show or execute
|
||||
if dry_run:
|
||||
console.print()
|
||||
console.print("[bold]Command:[/bold]")
|
||||
console.print()
|
||||
console.print(f" [dim]{' '.join(cmd)}[/dim]")
|
||||
console.print()
|
||||
return
|
||||
|
||||
# Execute with prepared environment variables
|
||||
# Don't print "Server started" or API info here - let sglang's logs speak for themselves
|
||||
# The actual startup takes time and these messages are misleading
|
||||
|
||||
# Print the command being executed
|
||||
console.print()
|
||||
console.print("[bold]Launching server with command:[/bold]")
|
||||
console.print()
|
||||
console.print(f" [dim]{' '.join(cmd)}[/dim]")
|
||||
console.print()
|
||||
|
||||
try:
|
||||
# Execute directly without intercepting output or signals
|
||||
# This allows direct output to terminal and Ctrl+C to work naturally
|
||||
process = subprocess.run(cmd, env=env)
|
||||
sys.exit(process.returncode)
|
||||
|
||||
except FileNotFoundError:
|
||||
from kt_kernel.cli.utils.sglang_checker import print_sglang_install_instructions
|
||||
|
||||
print_error(t("sglang_not_found"))
|
||||
console.print()
|
||||
print_sglang_install_instructions()
|
||||
raise typer.Exit(1)
|
||||
except Exception as e:
|
||||
print_error(f"Failed to start server: {e}")
|
||||
raise typer.Exit(1)
|
||||
|
||||
|
||||
# Dead code removed: _find_model_path() and _find_weights_path()
|
||||
# These functions were part of the old builtin model system
|
||||
|
||||
|
||||
def _check_conflicting_env_vars(kt_method: str, env: dict) -> None:
|
||||
"""Exit early if environment variables conflict with the chosen kt-method.
|
||||
|
||||
Receives the fully-assembled subprocess env dict (shell + kt config settings)
|
||||
so that variables injected via inference.env or advanced.env are also caught.
|
||||
Catches copy-paste mistakes such as keeping SGLANG_DSV4_2604_SUBMODE=2604B
|
||||
in the shell after switching from a MXFP4 launch to another method.
|
||||
"""
|
||||
dsv4_submode = env.get("SGLANG_DSV4_2604_SUBMODE", "")
|
||||
if dsv4_submode == "2604B" and (not kt_method or kt_method.upper() != "MXFP4"):
|
||||
print_error(
|
||||
f"SGLANG_DSV4_2604_SUBMODE=2604B is set but kt-method is "
|
||||
f"{kt_method!r} (not MXFP4). "
|
||||
f"This will raise a ValueError during model loading. "
|
||||
f"Either unset the variable (unset SGLANG_DSV4_2604_SUBMODE) "
|
||||
f"or switch to --kt-method MXFP4."
|
||||
)
|
||||
raise typer.Exit(1)
|
||||
|
||||
|
||||
def _build_sglang_command(
|
||||
model_path: Path,
|
||||
weights_path: Optional[Path],
|
||||
host: str,
|
||||
port: int,
|
||||
gpu_experts: int,
|
||||
cpu_threads: int,
|
||||
numa_nodes: int,
|
||||
tensor_parallel_size: int,
|
||||
kt_method: str,
|
||||
kt_gpu_prefill_threshold: int,
|
||||
attention_backend: str,
|
||||
max_total_tokens: int,
|
||||
max_running_requests: int,
|
||||
chunked_prefill_size: int,
|
||||
mem_fraction_static: float,
|
||||
watchdog_timeout: int,
|
||||
served_model_name: str,
|
||||
disable_shared_experts_fusion: bool,
|
||||
kt_numa_nodes: Optional[list[int]],
|
||||
tool_call_parser: Optional[str],
|
||||
reasoning_parser: Optional[str],
|
||||
settings,
|
||||
extra_model_params: Optional[dict] = None, # New parameter for additional params
|
||||
extra_cli_args: Optional[list[str]] = None, # Extra args from CLI to pass to sglang
|
||||
) -> list[str]:
|
||||
"""Build the SGLang launch command."""
|
||||
cmd = [
|
||||
sys.executable,
|
||||
"-m",
|
||||
"sglang.launch_server",
|
||||
"--host",
|
||||
host,
|
||||
"--port",
|
||||
str(port),
|
||||
"--model",
|
||||
str(model_path),
|
||||
]
|
||||
|
||||
# Add kt-kernel options
|
||||
# kt-kernel is needed for:
|
||||
# 1. Quantized models (when weights_path is provided)
|
||||
# 2. MoE models with CPU offloading (when kt-cpuinfer > 0 or kt-num-gpu-experts is configured)
|
||||
use_kt_kernel = False
|
||||
|
||||
# Check if we should use kt-kernel
|
||||
if weights_path:
|
||||
# Quantized model - always use kt-kernel
|
||||
use_kt_kernel = True
|
||||
elif cpu_threads > 0 or gpu_experts > 1:
|
||||
# CPU offloading configured - use kt-kernel
|
||||
use_kt_kernel = True
|
||||
|
||||
if use_kt_kernel:
|
||||
# Add kt-weight-path: use quantized weights if available, otherwise use model path
|
||||
weight_path_to_use = weights_path if weights_path else model_path
|
||||
|
||||
# Add kt-kernel configuration
|
||||
cmd.extend(
|
||||
[
|
||||
"--kt-weight-path",
|
||||
str(weight_path_to_use),
|
||||
"--kt-cpuinfer",
|
||||
str(cpu_threads),
|
||||
"--kt-threadpool-count",
|
||||
str(numa_nodes),
|
||||
"--kt-num-gpu-experts",
|
||||
str(gpu_experts),
|
||||
"--kt-method",
|
||||
kt_method,
|
||||
"--kt-gpu-prefill-token-threshold",
|
||||
str(kt_gpu_prefill_threshold),
|
||||
"--kt-enable-dynamic-expert-update", # Enable dynamic expert updates
|
||||
]
|
||||
)
|
||||
if kt_numa_nodes is not None:
|
||||
cmd.extend(["--kt-numa-nodes", *map(str, kt_numa_nodes)])
|
||||
|
||||
# Add SGLang options
|
||||
cmd.extend(
|
||||
[
|
||||
"--attention-backend",
|
||||
attention_backend,
|
||||
"--trust-remote-code",
|
||||
"--mem-fraction-static",
|
||||
str(mem_fraction_static),
|
||||
"--chunked-prefill-size",
|
||||
str(chunked_prefill_size),
|
||||
"--max-running-requests",
|
||||
str(max_running_requests),
|
||||
"--max-total-tokens",
|
||||
str(max_total_tokens),
|
||||
"--watchdog-timeout",
|
||||
str(watchdog_timeout),
|
||||
"--enable-mixed-chunk",
|
||||
"--tensor-parallel-size",
|
||||
str(tensor_parallel_size),
|
||||
"--enable-p2p-check",
|
||||
]
|
||||
)
|
||||
|
||||
# Add served model name if specified
|
||||
if served_model_name:
|
||||
cmd.extend(["--served-model-name", served_model_name])
|
||||
|
||||
# Add performance flags
|
||||
if disable_shared_experts_fusion:
|
||||
cmd.append("--disable-shared-experts-fusion")
|
||||
|
||||
# Add FP8 backend if using FP8 method
|
||||
if "FP8" in kt_method.upper():
|
||||
cmd.extend(["--fp8-gemm-backend", "triton"])
|
||||
|
||||
# Add parsers if specified
|
||||
if tool_call_parser:
|
||||
cmd.extend(["--tool-call-parser", tool_call_parser])
|
||||
if reasoning_parser:
|
||||
cmd.extend(["--reasoning-parser", reasoning_parser])
|
||||
|
||||
# Add any extra parameters from model defaults that weren't explicitly handled
|
||||
if extra_model_params:
|
||||
# List of parameters already handled above
|
||||
handled_params = {
|
||||
"kt-num-gpu-experts",
|
||||
"kt-cpuinfer",
|
||||
"kt-threadpool-count",
|
||||
"kt-numa-nodes",
|
||||
"kt-method",
|
||||
"kt-gpu-prefill-token-threshold",
|
||||
"attention-backend",
|
||||
"tensor-parallel-size",
|
||||
"max-total-tokens",
|
||||
"max-running-requests",
|
||||
"chunked-prefill-size",
|
||||
"mem-fraction-static",
|
||||
"watchdog-timeout",
|
||||
"served-model-name",
|
||||
"disable-shared-experts-fusion",
|
||||
}
|
||||
|
||||
for key, value in extra_model_params.items():
|
||||
if key not in handled_params:
|
||||
# Add unhandled parameters dynamically
|
||||
cmd.append(f"--{key}")
|
||||
if isinstance(value, bool):
|
||||
# Boolean flags don't need a value
|
||||
if not value:
|
||||
# For False boolean, skip the flag entirely
|
||||
cmd.pop() # Remove the flag we just added
|
||||
else:
|
||||
cmd.append(str(value))
|
||||
|
||||
# Add extra args from settings
|
||||
extra_args = settings.get("advanced.sglang_args", [])
|
||||
if extra_args:
|
||||
cmd.extend(extra_args)
|
||||
|
||||
# Add extra CLI args (user-provided options not defined in kt CLI)
|
||||
if extra_cli_args:
|
||||
cmd.extend(extra_cli_args)
|
||||
|
||||
return cmd
|
||||
|
||||
|
||||
def _interactive_model_selection(user_registry, settings) -> Optional[str]:
|
||||
"""Show interactive model selection interface.
|
||||
|
||||
Returns:
|
||||
Selected model name or None if cancelled.
|
||||
"""
|
||||
from rich.panel import Panel
|
||||
from rich.prompt import Prompt
|
||||
|
||||
# Get all user models
|
||||
all_models = user_registry.list_models()
|
||||
|
||||
if not all_models:
|
||||
console.print()
|
||||
print_warning("No models registered.")
|
||||
console.print()
|
||||
console.print(f" Add models with: [cyan]kt model scan[/cyan]")
|
||||
console.print(f" Or manually: [cyan]kt model add /path/to/model[/cyan]")
|
||||
console.print()
|
||||
return None
|
||||
|
||||
console.print()
|
||||
console.print(
|
||||
Panel.fit(
|
||||
"Select a model to run",
|
||||
border_style="cyan",
|
||||
)
|
||||
)
|
||||
console.print()
|
||||
|
||||
# Build choices list
|
||||
choices = []
|
||||
choice_map = {} # index -> model name
|
||||
|
||||
# Show all user models
|
||||
console.print(f"[bold green]Available Models:[/bold green]")
|
||||
console.print()
|
||||
|
||||
for i, model in enumerate(all_models, 1):
|
||||
# Check if path exists
|
||||
path_status = "✓" if model.path_exists() else "✗ Missing"
|
||||
console.print(f" [cyan][{i}][/cyan] [bold]{model.name}[/bold] [{path_status}]")
|
||||
console.print(f" [dim]{model.format} - {model.path}[/dim]")
|
||||
choices.append(str(i))
|
||||
choice_map[str(i)] = model.name
|
||||
|
||||
console.print()
|
||||
|
||||
# Add cancel option
|
||||
cancel_idx = str(len(choices) + 1)
|
||||
console.print(f" [cyan][{cancel_idx}][/cyan] [dim]Cancel[/dim]")
|
||||
choices.append(cancel_idx)
|
||||
console.print()
|
||||
|
||||
# Prompt for selection
|
||||
try:
|
||||
selection = Prompt.ask(
|
||||
"Select model",
|
||||
choices=choices,
|
||||
default="1" if choices else cancel_idx,
|
||||
)
|
||||
except KeyboardInterrupt:
|
||||
console.print()
|
||||
return None
|
||||
|
||||
if selection == cancel_idx:
|
||||
return None
|
||||
|
||||
return choice_map.get(selection)
|
||||
@@ -0,0 +1,52 @@
|
||||
"""
|
||||
SFT command for kt-cli.
|
||||
|
||||
Fine-tuning with LlamaFactory integration.
|
||||
"""
|
||||
|
||||
import typer
|
||||
|
||||
from kt_kernel.cli.i18n import t
|
||||
from kt_kernel.cli.utils.console import console
|
||||
|
||||
app = typer.Typer(help="Fine-tuning with LlamaFactory (coming soon)")
|
||||
|
||||
|
||||
@app.callback(invoke_without_command=True)
|
||||
def callback(ctx: typer.Context) -> None:
|
||||
"""Fine-tuning commands (coming soon)."""
|
||||
if ctx.invoked_subcommand is None:
|
||||
console.print()
|
||||
console.print(f"[yellow]{t('feature_coming_soon')}[/yellow]")
|
||||
console.print()
|
||||
console.print("[dim]kt sft train - Train a model[/dim]")
|
||||
console.print("[dim]kt sft chat - Chat with a trained model[/dim]")
|
||||
console.print("[dim]kt sft export - Export a trained model[/dim]")
|
||||
console.print()
|
||||
|
||||
|
||||
@app.command(name="train")
|
||||
def train() -> None:
|
||||
"""Train a model using LlamaFactory (coming soon)."""
|
||||
console.print()
|
||||
console.print(f"[yellow]{t('feature_coming_soon')}[/yellow]")
|
||||
console.print()
|
||||
raise typer.Exit(0)
|
||||
|
||||
|
||||
@app.command(name="chat")
|
||||
def chat() -> None:
|
||||
"""Chat with a trained model using LlamaFactory (coming soon)."""
|
||||
console.print()
|
||||
console.print(f"[yellow]{t('feature_coming_soon')}[/yellow]")
|
||||
console.print()
|
||||
raise typer.Exit(0)
|
||||
|
||||
|
||||
@app.command(name="export")
|
||||
def export() -> None:
|
||||
"""Export a trained model using LlamaFactory (coming soon)."""
|
||||
console.print()
|
||||
console.print(f"[yellow]{t('feature_coming_soon')}[/yellow]")
|
||||
console.print()
|
||||
raise typer.Exit(0)
|
||||
@@ -0,0 +1,102 @@
|
||||
"""
|
||||
Version command for kt-cli.
|
||||
|
||||
Displays version information for kt-cli and related packages.
|
||||
"""
|
||||
|
||||
import platform
|
||||
from typing import Optional
|
||||
|
||||
import typer
|
||||
|
||||
from kt_kernel.cli import __version__
|
||||
from kt_kernel.cli.i18n import t
|
||||
from kt_kernel.cli.utils.console import console, print_version_table
|
||||
from kt_kernel.cli.utils.environment import detect_cuda_version, get_installed_package_version
|
||||
|
||||
|
||||
def _get_sglang_info() -> str:
|
||||
"""Get sglang-kt version and installation source information."""
|
||||
from kt_kernel.cli.utils.sglang_checker import check_sglang_installation
|
||||
|
||||
info = check_sglang_installation()
|
||||
|
||||
if not info["installed"]:
|
||||
return t("version_not_installed")
|
||||
|
||||
# Get version from package metadata (prefer sglang-kt)
|
||||
version = get_installed_package_version("sglang-kt")
|
||||
if not version:
|
||||
version = get_installed_package_version("sglang")
|
||||
if not version:
|
||||
version = info.get("version") or "unknown"
|
||||
|
||||
# Determine source label
|
||||
if info.get("is_kvcache_fork"):
|
||||
if info["from_source"] and info.get("git_info"):
|
||||
git_remote = info["git_info"].get("remote", "")
|
||||
return f"{version} [dim](Source: {git_remote})[/dim]"
|
||||
elif info["editable"]:
|
||||
return f"{version} [dim](editable)[/dim]"
|
||||
else:
|
||||
return f"{version} [dim](sglang-kt)[/dim]"
|
||||
elif info["from_source"]:
|
||||
if info.get("git_info"):
|
||||
git_remote = info["git_info"].get("remote", "")
|
||||
return f"{version} [dim](Source: {git_remote})[/dim]"
|
||||
return f"{version} [dim](source)[/dim]"
|
||||
else:
|
||||
return f"{version} [dim](PyPI)[/dim]"
|
||||
|
||||
|
||||
def version(
|
||||
verbose: bool = typer.Option(False, "--verbose", "-v", help="Show detailed version info"),
|
||||
) -> None:
|
||||
"""Show version information."""
|
||||
console.print(f"\n[bold]{t('version_info')}[/bold] v{__version__}\n")
|
||||
|
||||
# Basic info
|
||||
versions = {
|
||||
t("version_python"): platform.python_version(),
|
||||
t("version_platform"): f"{platform.system()} {platform.release()}",
|
||||
}
|
||||
|
||||
# CUDA version
|
||||
cuda_version = detect_cuda_version()
|
||||
versions[t("version_cuda")] = cuda_version or t("version_cuda_not_found")
|
||||
|
||||
print_version_table(versions)
|
||||
|
||||
# Always show key packages with installation source
|
||||
console.print("\n[bold]Packages:[/bold]\n")
|
||||
|
||||
sglang_info = _get_sglang_info()
|
||||
key_packages = {
|
||||
t("version_kt_kernel"): get_installed_package_version("kt-kernel") or t("version_not_installed"),
|
||||
t("version_sglang"): sglang_info,
|
||||
}
|
||||
|
||||
print_version_table(key_packages)
|
||||
|
||||
# Show SGLang installation hint if not installed
|
||||
if sglang_info == t("version_not_installed"):
|
||||
from kt_kernel.cli.utils.sglang_checker import print_sglang_install_instructions
|
||||
|
||||
console.print()
|
||||
print_sglang_install_instructions()
|
||||
|
||||
if verbose:
|
||||
console.print("\n[bold]Additional Packages:[/bold]\n")
|
||||
|
||||
package_versions = {
|
||||
t("version_ktransformers"): get_installed_package_version("ktransformers") or t("version_not_installed"),
|
||||
t("version_llamafactory"): get_installed_package_version("llamafactory") or t("version_not_installed"),
|
||||
"typer": get_installed_package_version("typer") or t("version_not_installed"),
|
||||
"rich": get_installed_package_version("rich") or t("version_not_installed"),
|
||||
"torch": get_installed_package_version("torch") or t("version_not_installed"),
|
||||
"transformers": get_installed_package_version("transformers") or t("version_not_installed"),
|
||||
}
|
||||
|
||||
print_version_table(package_versions)
|
||||
|
||||
console.print()
|
||||
Reference in New Issue
Block a user