Text Generation
Safetensors
GGUF
English
tenaos
gemma
llama.cpp
lora
qlora
clinical
openmrs
conversational
Instructions to use beza4588/TenaOS with libraries, inference providers, notebooks, and local apps. Follow these links to get started.
- Notebooks
- Google Colab
- Kaggle
- Local Apps Settings
- llama.cpp
How to use beza4588/TenaOS with llama.cpp:
Install (macOS, Linux)
curl -LsSf https://llama.app/install.sh | sh # Start a local OpenAI-compatible server with a web UI: llama serve -hf beza4588/TenaOS:BF16 # Run inference directly in the terminal: llama cli -hf beza4588/TenaOS:BF16
Install from WinGet (Windows)
winget install llama.cpp # Start a local OpenAI-compatible server with a web UI: llama serve -hf beza4588/TenaOS:BF16 # Run inference directly in the terminal: llama cli -hf beza4588/TenaOS:BF16
Use pre-built binary
# Download pre-built binary from: # https://github.com/ggerganov/llama.cpp/releases # Start a local OpenAI-compatible server with a web UI: ./llama-server -hf beza4588/TenaOS:BF16 # Run inference directly in the terminal: ./llama-cli -hf beza4588/TenaOS:BF16
Build from source code
git clone https://github.com/ggerganov/llama.cpp.git cd llama.cpp cmake -B build cmake --build build -j --target llama-server llama-cli # Start a local OpenAI-compatible server with a web UI: ./build/bin/llama-server -hf beza4588/TenaOS:BF16 # Run inference directly in the terminal: ./build/bin/llama-cli -hf beza4588/TenaOS:BF16
Use Docker
docker model run hf.co/beza4588/TenaOS:BF16
- LM Studio
- Jan
- vLLM
How to use beza4588/TenaOS with vLLM:
Install from pip and serve model
# Install vLLM from pip: pip install vllm # Start the vLLM server: vllm serve "beza4588/TenaOS" # Call the server using curl (OpenAI-compatible API): curl -X POST "http://localhost:8000/v1/chat/completions" \ -H "Content-Type: application/json" \ --data '{ "model": "beza4588/TenaOS", "messages": [ { "role": "user", "content": "What is the capital of France?" } ] }'Use Docker
docker model run hf.co/beza4588/TenaOS:BF16
- Ollama
How to use beza4588/TenaOS with Ollama:
ollama run hf.co/beza4588/TenaOS:BF16
- Unsloth Desktop
- Pi
How to use beza4588/TenaOS with Pi:
Start the llama.cpp server
# Install llama.cpp: brew install llama.cpp # Start a local OpenAI-compatible server: llama serve -hf beza4588/TenaOS:BF16
Configure the model in Pi
# Install Pi: npm install -g @earendil-works/pi-coding-agent # Add to ~/.pi/agent/models.json: { "providers": { "llama-cpp": { "baseUrl": "http://localhost:8080/v1", "api": "openai-completions", "apiKey": "none", "models": [ { "id": "beza4588/TenaOS:BF16" } ] } } }Run Pi
# Start Pi in your project directory: pi
- Docker Model Runner
How to use beza4588/TenaOS with Docker Model Runner:
docker model run hf.co/beza4588/TenaOS:BF16
- Lemonade
How to use beza4588/TenaOS with Lemonade:
Pull the model
# Download Lemonade from https://lemonade-server.ai/ lemonade pull beza4588/TenaOS:BF16
Run and chat with the model
lemonade run user.TenaOS-BF16
List all available models
lemonade list
- Hermes Agent
How to use beza4588/TenaOS with Hermes Agent:
Start the llama.cpp server
# Install llama.cpp: brew install llama.cpp # Start a local OpenAI-compatible server: llama serve -hf beza4588/TenaOS:BF16
Configure Hermes
# Install Hermes: curl -fsSL https://hermes-agent.nousresearch.com/install.sh | bash hermes setup # Point Hermes at the local server: hermes config set model.provider custom hermes config set model.base_url http://127.0.0.1:8080/v1 hermes config set model.default beza4588/TenaOS:BF16
Run Hermes
hermes
- Atomic Chat
- OpenClaw
How to use beza4588/TenaOS with OpenClaw:
Start the llama.cpp server
# Install llama.cpp: brew install llama.cpp # Start a local OpenAI-compatible server: llama serve -hf beza4588/TenaOS:BF16
Configure OpenClaw
# Install OpenClaw: npm install -g openclaw@latest # Register the local server and set it as the default model: openclaw onboard --non-interactive --mode local \ --auth-choice custom-api-key \ --custom-base-url http://127.0.0.1:8080/v1 \ --custom-model-id "beza4588/TenaOS:BF16" \ --custom-provider-id llama-cpp \ --custom-compatibility openai \ --custom-text-input \ --accept-risk \ --skip-health
Run OpenClaw
openclaw agent --local --agent main --message "Hello from Hugging Face"
Download training_code/train_unsloth_lora.py from beza4588/TenaOS: direct link, hf CLI and curl.
- Browser
- Download file 19.1 kB
-
https://huggingface.co/beza4588/TenaOS/resolve/main/training_code/train_unsloth_lora.py
- Command line
-
hf download hf://beza4588/TenaOS/training_code/train_unsloth_lora.py
-
curl -L -o train_unsloth_lora.py https://huggingface.co/beza4588/TenaOS/resolve/main/training_code/train_unsloth_lora.py
19.1 kB
| #!/usr/bin/env python3 | |
| """Train the TenaOS single task-tagged Gemma 4 LoRA adapter with Unsloth.""" | |
| from __future__ import annotations | |
| import argparse | |
| import json | |
| import math | |
| import traceback | |
| from datetime import datetime, timezone | |
| from pathlib import Path | |
| from typing import Any | |
| DEFAULT_BASE_MODEL = "unsloth/gemma-4-E4B-it" | |
| def parse_args() -> argparse.Namespace: | |
| parser = argparse.ArgumentParser(description=__doc__) | |
| parser.add_argument("--train-jsonl", type=Path, required=True) | |
| parser.add_argument("--validation-jsonl", type=Path, required=True) | |
| parser.add_argument("--output-dir", type=Path, required=True) | |
| parser.add_argument("--base-model", default=DEFAULT_BASE_MODEL) | |
| parser.add_argument("--merge-output-dir", type=Path, help="Optional directory for merged weights.") | |
| parser.add_argument( | |
| "--max-seq-length", | |
| type=int, | |
| default=32768, | |
| help=( | |
| "Real multi-turn conversations run much longer than the old flattened single-completion " | |
| "rows. Measured char-length distribution across the reconstructed corpus (rough proxy, " | |
| "~3.5 chars/token): form median ~22.4K chars / p95 ~50.5K chars / max ~125.5K chars; " | |
| "report median ~9.8K / p95 ~40.9K / max ~66K; cds/patient_education/scribe all comfortably " | |
| "under 16K chars. 32768 tokens covers the large majority without the extreme few-outlier " | |
| "tail; those are truncated by SFTConfig's max_length rather than raising the default so high " | |
| "that most batches waste compute on padding." | |
| ), | |
| ) | |
| parser.add_argument("--epochs", type=float, default=3.0) | |
| parser.add_argument("--max-steps", type=int, default=-1, help="Optional cap for smoke tests or short runs.") | |
| parser.add_argument("--per-device-train-batch-size", type=int, default=1) | |
| parser.add_argument("--gradient-accumulation-steps", type=int, default=8) | |
| parser.add_argument("--learning-rate", type=float, default=2e-4) | |
| parser.add_argument("--warmup-ratio", type=float, default=0.03) | |
| parser.add_argument("--logging-steps", type=int, default=10) | |
| parser.add_argument("--eval-steps", type=int, default=100) | |
| parser.add_argument("--save-steps", type=int, default=250) | |
| parser.add_argument("--seed", type=int, default=3407) | |
| parser.add_argument("--load-in-4bit", action="store_true", help="Use QLoRA fallback if BF16 LoRA does not fit.") | |
| parser.add_argument("--dataset-num-proc", type=int, default=4) | |
| parser.add_argument("--packing", action="store_true", help="Enable TRL packing for short examples.") | |
| parser.add_argument( | |
| "--chat-template", | |
| default=None, | |
| help=( | |
| "Optional Unsloth canned chat_template name (see unsloth.chat_templates.CHAT_TEMPLATES). " | |
| "Defaults to the base model's own tokenizer chat_template (recommended: this is the same " | |
| "template baked into the merged GGUF that llama.cpp renders with --jinja at serve time, " | |
| "so training and inference stay byte-identical without depending on Unsloth shipping a " | |
| "matching canned template for this specific base model)." | |
| ), | |
| ) | |
| parser.add_argument( | |
| "--instruction-part", | |
| default="<|turn>user\n", | |
| help=( | |
| "Turn-start marker for non-assistant turns, used by train_on_responses_only masking. " | |
| "NOTE: this Gemma 4 base model's chat template uses '<|turn>role ... <turn|>' markers, " | |
| "NOT the classic Gemma 2/3 '<start_of_turn>role ... <end_of_turn>' tokens — verified by " | |
| "rendering a sample conversation with the tokenizer's own apply_chat_template. Using the " | |
| "wrong markers silently zeroes out every example's loss mask (train_on_responses_only " | |
| "finds no matches), which crashes the RandomSampler with num_samples=0." | |
| ), | |
| ) | |
| parser.add_argument( | |
| "--response-part", | |
| default="<|turn>model\n", | |
| help="Turn-start marker for assistant turns, used by train_on_responses_only masking.", | |
| ) | |
| parser.add_argument( | |
| "--no-response-only-masking", | |
| action="store_true", | |
| help="Disable response-only loss masking (NOT recommended — trains loss over tool/user tokens too).", | |
| ) | |
| parser.add_argument( | |
| "--use-flex-attention", | |
| action="store_true", | |
| help=( | |
| "Force torch's flex_attention backend instead of Unsloth's default sdpa. NOT " | |
| "recommended: under Unsloth's custom gradient checkpointing the layer recompute runs " | |
| "outside torch.compile, so flex_attention's backward dispatches to its eager dense " | |
| "decomposition (torch/_higher_order_ops/flex_attention.py::sdpa_dense_backward), which " | |
| "materializes full quadratic (B, H, N, N) score/grad tensors — ~9 GiB each at " | |
| "N=24576 with 8 heads — and reliably OOMs an 80 GB A100 on the first backward pass. " | |
| "The original rationale (head_dim 512 exceeding fused-kernel limits) was wrong: this " | |
| "model's config has head_dim=256, which sdpa's fused flash/mem-efficient kernels fully " | |
| "support." | |
| ), | |
| ) | |
| parser.add_argument( | |
| "--no-length-filter", | |
| action="store_true", | |
| help=( | |
| "Disable dropping examples whose rendered token length exceeds --max-seq-length. By default " | |
| "such outliers are dropped entirely rather than silently truncated by SFTConfig's max_length, " | |
| "since truncation can cut a conversation off mid tool-call/JSON payload and teach the model a " | |
| "corrupted, incomplete turn. Measured on the current corpus, only ~0.1% of examples (the " | |
| "extreme long tail of the 'form' task) exceed 24576 tokens, so dropping them is cheap." | |
| ), | |
| ) | |
| return parser.parse_args() | |
| def main() -> None: | |
| args = parse_args() | |
| args.output_dir.mkdir(parents=True, exist_ok=True) | |
| try: | |
| from unsloth import FastVisionModel | |
| from unsloth.chat_templates import CHAT_TEMPLATES, get_chat_template, train_on_responses_only | |
| import torch | |
| from datasets import load_dataset | |
| from trl import SFTConfig, SFTTrainer | |
| except ImportError as exc: | |
| raise SystemExit( | |
| "Missing training dependencies. Install Unsloth, TRL, datasets, transformers, peft, and torch " | |
| "in the training environment before running this script." | |
| ) from exc | |
| model, tokenizer = FastVisionModel.from_pretrained( | |
| args.base_model, | |
| load_in_4bit=args.load_in_4bit, | |
| use_gradient_checkpointing="unsloth", | |
| max_seq_length=args.max_seq_length, | |
| ) | |
| # Keep Unsloth's default sdpa, but force the repeat-KV path instead of enable_gqa. | |
| # | |
| # Verified by intercepting the actual SDPA calls during a 24576-token forward: | |
| # - sliding-window layers (35/42): q/k (1, 8, N, 256) + boolean mask | |
| # -> fused memory-efficient kernel, linear memory. Fine as-is. | |
| # - global full-attention layers (7/42): q (1, 8, N, 512), k/v (1, 2, N, 512) | |
| # with enable_gqa=True and is_causal. | |
| # No fused SDPA kernel supports head_dim=512 *with* enable_gqa (flash caps at | |
| # head_dim 256; the memory-efficient kernel rejects enable_gqa), so those 7 layers | |
| # silently dispatch to the quadratic-memory math kernel, whose backward materializes | |
| # an (8, N, N) fp32 tensor -- an 18 GiB single allocation at N=24576, which OOMs the | |
| # 80 GB A100. With K/V repeated to the full 8 heads the memory-efficient kernel | |
| # accepts head_dim=512 (measured: 2.49 GiB peak for fwd+bwd of one such layer at | |
| # N=24576, vs OOM before). repeat_kv is mathematically identical to enable_gqa, | |
| # so this changes memory/speed only, never outputs. | |
| # | |
| # (A previous revision instead forced flex_attention here, which OOMed even harder: | |
| # under Unsloth's gradient checkpointing the recompute runs uncompiled, and eager | |
| # flex_attention's backward decomposes to dense sdpa_dense_backward with full | |
| # (B, H, N, N) score tensors on *every* layer.) | |
| if args.use_flex_attention: | |
| model.set_attn_implementation("flex_attention") | |
| print(f"Attention implementation forced to: {model.config._attn_implementation}") | |
| else: | |
| import transformers.integrations.sdpa_attention as _sdpa_integration | |
| _sdpa_integration.use_gqa_in_sdpa = lambda attention_mask, key: False | |
| print( | |
| "SDPA enable_gqa disabled (forcing repeat_kv) so the 512-dim global-attention " | |
| "layers use the fused memory-efficient kernel instead of the quadratic math fallback." | |
| ) | |
| if args.chat_template: | |
| if args.chat_template not in CHAT_TEMPLATES: | |
| raise SystemExit( | |
| f"--chat-template={args.chat_template!r} is not a known Unsloth canned template " | |
| f"(available: {sorted(CHAT_TEMPLATES.keys())}). Omit --chat-template to use the base " | |
| "model's own tokenizer chat_template instead (recommended)." | |
| ) | |
| tokenizer = get_chat_template(tokenizer, chat_template=args.chat_template) | |
| elif not getattr(tokenizer, "chat_template", None): | |
| raise SystemExit( | |
| f"Base model {args.base_model!r} has no tokenizer chat_template and no --chat-template " | |
| "override was given. A previous run hardcoded chat_template='gemma-4-thinking', which does " | |
| "not exist in this Unsloth version's CHAT_TEMPLATES registry and would crash here — pass an " | |
| "explicit --chat-template only if you have verified it renders tool-role turns the same way " | |
| "llama.cpp's --jinja rendering of the merged GGUF does (see the template-check step)." | |
| ) | |
| print(f"Using chat_template (first 200 chars): {str(tokenizer.chat_template)[:200]!r}") | |
| model = FastVisionModel.get_peft_model( | |
| model, | |
| finetune_vision_layers=False, | |
| finetune_language_layers=True, | |
| finetune_attention_modules=True, | |
| finetune_mlp_modules=True, | |
| r=16, | |
| lora_alpha=32, | |
| # NOTE: the installed peft version's MoE ParamWrapper path (used for | |
| # `experts.gate_up_proj` / `experts.down_proj`) raises | |
| # `ValueError: lora.ParamWrapper does not work with lora_dropout != 0` | |
| # for this model's grouped-expert parameters, so dropout must stay 0. | |
| lora_dropout=0.0, | |
| bias="none", | |
| random_state=args.seed, | |
| use_rslora=False, | |
| loftq_config=None, | |
| target_modules="all-linear", | |
| ) | |
| dataset = load_dataset( | |
| "json", | |
| data_files={ | |
| "train": str(args.train_jsonl), | |
| "validation": str(args.validation_jsonl), | |
| }, | |
| ) | |
| def formatting_prompts_func(examples: dict[str, Any]) -> dict[str, list[str]]: | |
| texts = [ | |
| tokenizer.apply_chat_template(convo, tokenize=False, add_generation_prompt=False).removeprefix("<bos>") | |
| for convo in examples["conversations"] | |
| ] | |
| return {"text": texts} | |
| dataset = dataset.map( | |
| formatting_prompts_func, | |
| batched=True, | |
| num_proc=args.dataset_num_proc, | |
| remove_columns=dataset["train"].column_names, | |
| ) | |
| if not args.no_length_filter: | |
| def _add_token_length(examples: dict[str, Any]) -> dict[str, list[int]]: | |
| # Gemma4's tokenizer is a full multimodal processor; it requires `text=` | |
| # passed as an explicit keyword (positional args get swallowed as `images` | |
| # by Unsloth's patched __call__ wrapper, leaving text=None internally). | |
| # padding=False is required too: batched calls otherwise pad every row in | |
| # the (default size-1000) map batch out to that batch's single longest | |
| # sequence, which silently inflates the reported length of every short | |
| # example that happens to share a batch with one long outlier. | |
| return { | |
| "_n_tokens": [ | |
| len(ids) | |
| for ids in tokenizer( | |
| text=examples["text"], add_special_tokens=False, padding=False | |
| )["input_ids"] | |
| ] | |
| } | |
| for split in ("train", "validation"): | |
| before = len(dataset[split]) | |
| dataset[split] = dataset[split].map(_add_token_length, batched=True, num_proc=args.dataset_num_proc) | |
| dataset[split] = dataset[split].filter( | |
| lambda ex: ex["_n_tokens"] <= args.max_seq_length, | |
| num_proc=args.dataset_num_proc, | |
| ) | |
| dataset[split] = dataset[split].remove_columns(["_n_tokens"]) | |
| after = len(dataset[split]) | |
| print( | |
| f"Length filter ({split}): dropped {before - after}/{before} examples exceeding " | |
| f"--max-seq-length={args.max_seq_length} tokens (kept {after})." | |
| ) | |
| else: | |
| print( | |
| "WARNING: --no-length-filter set; over-length examples will be silently truncated by " | |
| "SFTConfig's max_length instead of being dropped." | |
| ) | |
| train_size = len(dataset["train"]) | |
| effective_batch = max(1, args.per_device_train_batch_size * args.gradient_accumulation_steps) | |
| estimated_steps = max(1, math.ceil(train_size / effective_batch) * max(1, math.ceil(args.epochs))) | |
| warmup_steps = max(1, int(estimated_steps * args.warmup_ratio)) | |
| train_args = SFTConfig( | |
| output_dir=str(args.output_dir), | |
| num_train_epochs=args.epochs, | |
| max_steps=args.max_steps, | |
| per_device_train_batch_size=args.per_device_train_batch_size, | |
| gradient_accumulation_steps=args.gradient_accumulation_steps, | |
| learning_rate=args.learning_rate, | |
| warmup_steps=warmup_steps, | |
| logging_steps=args.logging_steps, | |
| # In-loop evaluation is disabled: Unsloth's patched unsloth_prediction_step | |
| # (unsloth/models/rl.py) unconditionally sets UNSLOTH_RETURN_LOGITS=1 and | |
| # calls compute_loss(..., return_outputs=True) during eval, ignoring | |
| # prediction_loss_only entirely. That materializes the full | |
| # (batch, seq_len, ~262K vocab) logits tensor and casts it to fp32 inside | |
| # the forward pass itself -- verified to OOM (41.6 GiB single allocation) | |
| # on this model's vocab size regardless of TrainingArguments settings. | |
| # prediction_loss_only only affects what the Trainer keeps afterward, by | |
| # which point the OOM has already happened. Real validation/quality | |
| # checks happen post-training via evaluate_lora_ab.py against the held-out | |
| # test split instead; save_steps below still gives periodic checkpoints. | |
| eval_strategy="no", | |
| save_steps=args.save_steps, | |
| save_total_limit=3, | |
| optim="adamw_8bit", | |
| weight_decay=0.01, | |
| lr_scheduler_type="linear", | |
| seed=args.seed, | |
| bf16=torch.cuda.is_available() and torch.cuda.is_bf16_supported(), | |
| fp16=torch.cuda.is_available() and not torch.cuda.is_bf16_supported(), | |
| dataset_num_proc=args.dataset_num_proc, | |
| dataset_text_field="text", | |
| max_length=args.max_seq_length, | |
| packing=args.packing, | |
| report_to="none", | |
| ) | |
| trainer = SFTTrainer( | |
| model=model, | |
| processing_class=tokenizer, | |
| train_dataset=dataset["train"], | |
| eval_dataset=dataset["validation"], | |
| args=train_args, | |
| ) | |
| if not args.no_response_only_masking: | |
| # Every training example is now a real multi-turn conversation | |
| # (system/user/assistant tool-calls/tool results) reconstructed from | |
| # production traces, not a single flattened completion. Without this, | |
| # SFT loss would run over system/user/tool tokens too — including | |
| # tool results the model never generates itself — which both wastes | |
| # signal and actively trains the model to try to "predict" search | |
| # results and prior context verbatim. Mask to assistant-authored | |
| # tokens only, matched via the chat template's own turn markers. | |
| trainer = train_on_responses_only( | |
| trainer, | |
| instruction_part=args.instruction_part, | |
| response_part=args.response_part, | |
| ) | |
| print( | |
| f"Response-only masking enabled (instruction_part={args.instruction_part!r}, " | |
| f"response_part={args.response_part!r})." | |
| ) | |
| else: | |
| print("WARNING: response-only masking disabled via --no-response-only-masking.") | |
| train_result = trainer.train() | |
| trainer.save_model(str(args.output_dir)) | |
| tokenizer.save_pretrained(str(args.output_dir)) | |
| metadata = { | |
| "schema_version": "tenaos_lora_training_run_v1", | |
| "created_at": datetime.now(timezone.utc).isoformat(), | |
| "base_model": args.base_model, | |
| "train_jsonl": str(args.train_jsonl), | |
| "validation_jsonl": str(args.validation_jsonl), | |
| "output_dir": str(args.output_dir), | |
| "load_in_4bit": args.load_in_4bit, | |
| "chat_template_name": args.chat_template or "<tokenizer_native>", | |
| "chat_template_prefix": str(tokenizer.chat_template)[:200], | |
| "response_only_masking": not args.no_response_only_masking, | |
| "instruction_part": args.instruction_part, | |
| "response_part": args.response_part, | |
| "hyperparameters": { | |
| "lora_rank": 16, | |
| "lora_alpha": 32, | |
| "lora_dropout": 0.0, | |
| "epochs": args.epochs, | |
| "max_steps": args.max_steps, | |
| "max_seq_length": args.max_seq_length, | |
| "per_device_train_batch_size": args.per_device_train_batch_size, | |
| "gradient_accumulation_steps": args.gradient_accumulation_steps, | |
| "learning_rate": args.learning_rate, | |
| "warmup_steps": warmup_steps, | |
| "seed": args.seed, | |
| "target_modules": "all-linear", | |
| "finetune_language_layers": True, | |
| "finetune_attention_modules": True, | |
| "finetune_mlp_modules": True, | |
| "finetune_vision_layers": False, | |
| }, | |
| "train_metrics": train_result.metrics, | |
| } | |
| (args.output_dir / "training_metadata.json").write_text( | |
| json.dumps(metadata, indent=2, ensure_ascii=False, sort_keys=True) + "\n", | |
| encoding="utf-8", | |
| ) | |
| if args.merge_output_dir: | |
| args.merge_output_dir.mkdir(parents=True, exist_ok=True) | |
| model.save_pretrained_merged(str(args.merge_output_dir), tokenizer, save_method="merged_16bit") | |
| metadata["merge_output_dir"] = str(args.merge_output_dir) | |
| (args.output_dir / "training_metadata.json").write_text( | |
| json.dumps(metadata, indent=2, ensure_ascii=False, sort_keys=True) + "\n", | |
| encoding="utf-8", | |
| ) | |
| print(json.dumps(metadata, indent=2, ensure_ascii=False, sort_keys=True)) | |
| if __name__ == "__main__": | |
| try: | |
| main() | |
| except BaseException: | |
| traceback.print_exc() | |
| raise | |