Text Generation
Transformers
Safetensors
GGUF
English
llama
tiny-model
from-scratch
model-growth
conversational
multi-turn
tool-use
agent-harness
retrieval-augmented
multi-hop-qa
question-answering
attribution
humble-ai
small-language-model
muon
text-generation-inference
Instructions to use textilelabs/Loom-Tapestry-3 with libraries, inference providers, notebooks, and local apps. Follow these links to get started.
- Libraries
- Transformers
How to use textilelabs/Loom-Tapestry-3 with Transformers:
# Use a pipeline as a high-level helper from transformers import pipeline pipe = pipeline("text-generation", model="textilelabs/Loom-Tapestry-3") messages = [ {"role": "user", "content": "Who are you?"}, ] pipe(messages)# Load model directly from transformers import AutoTokenizer, AutoModelForCausalLM tokenizer = AutoTokenizer.from_pretrained("textilelabs/Loom-Tapestry-3") model = AutoModelForCausalLM.from_pretrained("textilelabs/Loom-Tapestry-3", device_map="auto") - Notebooks
- Google Colab
- Kaggle
- Local Apps Settings
- llama.cpp
How to use textilelabs/Loom-Tapestry-3 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 textilelabs/Loom-Tapestry-3:F16 # Run inference directly in the terminal: llama cli -hf textilelabs/Loom-Tapestry-3:F16
Install from WinGet (Windows)
winget install llama.cpp # Start a local OpenAI-compatible server with a web UI: llama serve -hf textilelabs/Loom-Tapestry-3:F16 # Run inference directly in the terminal: llama cli -hf textilelabs/Loom-Tapestry-3:F16
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 textilelabs/Loom-Tapestry-3:F16 # Run inference directly in the terminal: ./llama-cli -hf textilelabs/Loom-Tapestry-3:F16
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 textilelabs/Loom-Tapestry-3:F16 # Run inference directly in the terminal: ./build/bin/llama-cli -hf textilelabs/Loom-Tapestry-3:F16
Use Docker
docker model run hf.co/textilelabs/Loom-Tapestry-3:F16
- LM Studio
- Jan
- vLLM
How to use textilelabs/Loom-Tapestry-3 with vLLM:
Install from pip and serve model
# Install vLLM from pip: pip install vllm # Start the vLLM server: vllm serve "textilelabs/Loom-Tapestry-3" # Call the server using curl (OpenAI-compatible API): curl -X POST "http://localhost:8000/v1/chat/completions" \ -H "Content-Type: application/json" \ --data '{ "model": "textilelabs/Loom-Tapestry-3", "messages": [ { "role": "user", "content": "What is the capital of France?" } ] }'Use Docker
docker model run hf.co/textilelabs/Loom-Tapestry-3:F16
- SGLang
How to use textilelabs/Loom-Tapestry-3 with SGLang:
Install from pip and serve model
# Install SGLang from pip: pip install sglang # Start the SGLang server: python3 -m sglang.launch_server \ --model-path "textilelabs/Loom-Tapestry-3" \ --host 0.0.0.0 \ --port 30000 # Call the server using curl (OpenAI-compatible API): curl -X POST "http://localhost:30000/v1/chat/completions" \ -H "Content-Type: application/json" \ --data '{ "model": "textilelabs/Loom-Tapestry-3", "messages": [ { "role": "user", "content": "What is the capital of France?" } ] }'Use Docker images
docker run --gpus all \ --shm-size 32g \ -p 30000:30000 \ -v ~/.cache/huggingface:/root/.cache/huggingface \ --env "HF_TOKEN=<secret>" \ --ipc=host \ lmsysorg/sglang:latest \ python3 -m sglang.launch_server \ --model-path "textilelabs/Loom-Tapestry-3" \ --host 0.0.0.0 \ --port 30000 # Call the server using curl (OpenAI-compatible API): curl -X POST "http://localhost:30000/v1/chat/completions" \ -H "Content-Type: application/json" \ --data '{ "model": "textilelabs/Loom-Tapestry-3", "messages": [ { "role": "user", "content": "What is the capital of France?" } ] }' - Ollama
How to use textilelabs/Loom-Tapestry-3 with Ollama:
ollama run hf.co/textilelabs/Loom-Tapestry-3:F16
- Unsloth Desktop
- Docker Model Runner
How to use textilelabs/Loom-Tapestry-3 with Docker Model Runner:
docker model run hf.co/textilelabs/Loom-Tapestry-3:F16
- Lemonade
How to use textilelabs/Loom-Tapestry-3 with Lemonade:
Pull the model
# Download Lemonade from https://lemonade-server.ai/ lemonade pull textilelabs/Loom-Tapestry-3:F16
Run and chat with the model
lemonade run user.Loom-Tapestry-3-F16
List all available models
lemonade list
- Atomic Chat
Download harness.py from textilelabs/Loom-Tapestry-3: direct link, hf CLI and curl.
- Browser
- Download file 10.6 kB
-
https://huggingface.co/textilelabs/Loom-Tapestry-3/resolve/99eb311d130a5e6fd9b0cf8423ce23065449d508/harness.py
- Command line
-
hf download hf://textilelabs/Loom-Tapestry-3@99eb311d130a5e6fd9b0cf8423ce23065449d508/harness.py
-
curl -L -o harness.py https://huggingface.co/textilelabs/Loom-Tapestry-3/resolve/99eb311d130a5e6fd9b0cf8423ce23065449d508/harness.py
10.6 kB
| #!/usr/bin/env python3 | |
| """Loom harness — the search half of Loom Spark 3. | |
| The model never searches. It decides a lookup is needed and writes the query: | |
| <lookup>france</lookup> | |
| This script does the rest: searches Wikipedia, finds the ONE sentence most likely to | |
| hold the answer, hands it back as a <result>, and lets the model answer from it. | |
| python3 harness.py "what's the capital of france" | |
| python3 harness.py # interactive | |
| python3 harness.py --no-tools "who are you" | |
| python3 harness.py --show "who wrote hamlet" # print what was searched and read | |
| How it finds the answer, and why each step exists (all measured on live questions): | |
| * searches the model's query AND the subject it can see in your question — | |
| "whats the capital of france" searched as-is returns "Capital city" and "Das Kapital" | |
| * prefers the real article over lists, films, albums and disambiguation pages | |
| * reads the article's intro first, and further only when the intro has no answer of | |
| the right kind (a height with a unit, a year, a number, a name) | |
| * strips brackets and pronunciation guides, so real text looks like training text | |
| * hands back ONE sentence. A 340-character window found the answer more often but the | |
| model misread it four times in five; one sentence doubled the final score (15% -> 30%) | |
| Swap search() for anything you like — the contract is text in, one sentence out. | |
| Wikipedia needs no API key. Stdlib only. | |
| """ | |
| from __future__ import annotations | |
| import argparse, json, re, ssl, sys, time, urllib.error, urllib.parse, urllib.request | |
| try: # macOS system Python often lacks a CA bundle | |
| import certifi | |
| SSL_CTX = ssl.create_default_context(cafile=certifi.where()) | |
| except Exception: | |
| SSL_CTX = ssl.create_default_context() | |
| OLLAMA = "http://localhost:11434/api/generate" | |
| MODEL = "hf.co/textilelabs/Loom-Tapestry-3" | |
| API = "https://en.wikipedia.org/w/api.php?" | |
| # Wikipedia returns 403 without a descriptive User-Agent. | |
| UA = {"User-Agent": "LoomHarness/3.0 (Textile Labs; https://huggingface.co/textilelabs)"} | |
| LOOKUP = re.compile(r"<lookup>(.*?)</lookup>", re.S) | |
| _cache: dict = {} | |
| # ------------------------------------------------------------------- the model | |
| def loom(prompt: str, n: int = 64) -> str: | |
| body = json.dumps({"model": MODEL, "prompt": prompt, "raw": True, "stream": False, | |
| "options": {"temperature": 0, "num_predict": n, | |
| "stop": ["<|eot|>", "<user>", "<result>"]}}).encode() | |
| req = urllib.request.Request(OLLAMA, data=body, headers={"Content-Type": "application/json"}) | |
| with urllib.request.urlopen(req, timeout=120) as r: | |
| return json.load(r)["response"].strip() | |
| # ------------------------------------------------------------------ wikipedia | |
| def _get(params: dict) -> dict: | |
| key = json.dumps(params, sort_keys=True) | |
| if key in _cache: | |
| return _cache[key] | |
| for attempt in range(3): | |
| try: | |
| with urllib.request.urlopen(urllib.request.Request( | |
| API + urllib.parse.urlencode(params), headers=UA), | |
| context=SSL_CTX, timeout=20) as r: | |
| _cache[key] = json.load(r) | |
| return _cache[key] | |
| except urllib.error.HTTPError as e: | |
| if e.code == 429: | |
| time.sleep(3 * (attempt + 1)); continue | |
| raise | |
| raise RuntimeError("Wikipedia rate limit") | |
| def search(q: str, n: int = 3) -> list: | |
| return [h["title"] for h in _get({"action": "query", "list": "search", "srsearch": q, | |
| "format": "json", "srlimit": n})["query"]["search"]] | |
| def _extract(title: str, intro: bool) -> str: | |
| p = {"action": "query", "prop": "extracts", "explaintext": 1, "titles": title, | |
| "format": "json", "redirects": 1} | |
| if intro: | |
| p["exintro"] = 1 | |
| return next(iter(_get(p)["query"]["pages"].values())).get("extract", "") or "" | |
| # ------------------------------------------------------------------ the finder | |
| SENT = re.compile(r"(?<=[.!?])\s+(?=[A-Z0-9])") | |
| PAREN = re.compile(r"\s*\([^()]*\)") | |
| HEADING = re.compile(r"^\s*=+[^=]+=+\s*$", re.M) | |
| STOP = set(("what whats who whos whom whose when where which why how is are was were be the a an " | |
| "of in on to for does did do by from with as at and or that this it its there tell me " | |
| "please can you many much").split()) | |
| ATTR = set(("capital city height tall high elevation population largest biggest smallest longest " | |
| "shortest tallest highest deepest first last symbol chemical language languages spoken " | |
| "legs year date end ended sink sank invented inventor discovered discovery developed " | |
| "wrote written author painted painter president founded born died age old size area " | |
| "distance speed").split()) | |
| JUNK = re.compile(r"^(lists? of|outline of|index of|timeline of)\b|\((film|album|song|band|" | |
| r"novel|play|tv series|musical|opera|video game|book|composition|poem)\)|" | |
| r"\bdisambiguation\b", re.I) | |
| def keywords(t: str) -> list: | |
| return [w for w in re.findall(r"[^\W_]+", t.lower()) if w not in STOP] | |
| def subject(question: str) -> str: | |
| kw = keywords(question) | |
| return " ".join(k for k in kw if k not in ATTR) or " ".join(kw) | |
| def clean(t: str) -> str: | |
| prev = None | |
| while prev != t: | |
| prev, t = t, PAREN.sub("", t) | |
| return re.sub(r"\s+", " ", t.replace(" ,", ",")).strip() | |
| def _hard(q: str, s: str) -> float: | |
| """The answer is of the right KIND: a height with a unit, a year, a number, a name.""" | |
| b = 0.0 | |
| if re.search(r"\b(how tall|how high|height|elevation)\b", q): | |
| b += 2.0 if re.search(r"\d[\d,.]*\s*(m|metres|meters|ft|feet|km)\b", s) else 0 | |
| if re.search(r"\b(when|what year|which year|what date)\b", q): | |
| b += 2.0 if re.search(r"\b(1\d{3}|20\d{2})\b", s) else 0 | |
| if re.search(r"\b(how many|how much|population|number of)\b", q): | |
| b += 1.5 if re.search(r"\d", s) else 0 | |
| if re.search(r"\bwho\b", q): | |
| b += 1.5 if re.search(r"\b[A-Z][a-z]+ [A-Z][a-z]+", s) else 0 | |
| if re.search(r"\bsymbol\b", q): | |
| b += 2.0 if re.search(r"\bsymbol\b", s, re.I) else 0 | |
| if re.search(r"\bcapital\b", q): | |
| b += 2.0 if re.search(r"\bcapital\b", s, re.I) else 0 | |
| return b | |
| def _kind(q: str, s: str) -> float: | |
| b, sl = _hard(q, s), s.lower() | |
| if re.search(r"\b(how tall|how high|height|elevation)\b", q): | |
| b += 1.5 if re.search(r"\b(summit|elevation|height|above sea level|highest|stands)\b", sl) else -0.5 | |
| if re.search(r"\b(end|ended|finish|finished)\b", q): | |
| b += 1.5 if re.search(r"\b(ended|end of|surrender|surrendered|concluded|finished)\b", sl) else -0.5 | |
| if re.search(r"\bpopulation\b", q): | |
| b += 2.0 if re.search(r"\d{1,3}(,\d{3})+|\d+(\.\d+)?\s*(million|billion)", s) else -1.0 | |
| if re.search(r"\b(invent|invented|inventor|discovered|wrote|painted|composed|founded)\b", q): | |
| b += 1.0 if re.search(r"\b[A-Z][a-z]+ (?:[A-Z][a-z]+ )?[A-Z][a-z]+\b", s) else 0.0 | |
| return b | |
| def find(query: str, question: str) -> tuple: | |
| """One sentence most likely to hold the answer, and the article it came from.""" | |
| q = question.lower() | |
| subj = subject(question) | |
| pool = {} | |
| for tq in dict.fromkeys(x for x in (query.strip(), subj, " ".join(keywords(question))) if x): | |
| for rank, t in enumerate(search(tq, 3)): | |
| tl = t.lower() | |
| s = (4.0 if tl in (subj, query.strip().lower()) else 2.0 if subj and tl.startswith(subj) else 0.0) | |
| s += -4.0 if JUNK.search(t) else 0.0 | |
| pool[t] = max(pool.get(t, -1e9), s - 0.3 * rank) | |
| qk = list(dict.fromkeys(keywords(question) + keywords(query))) | |
| top = sorted(pool.items(), key=lambda x: -x[1])[:3] | |
| typed = bool(re.search(r"\b(how tall|how high|height|elevation|when|what year|which year|" | |
| r"how many|how much|population|who|symbol|capital)\b", q)) | |
| best = (-1e9, "", "") | |
| for intro in (True, False): | |
| found_kind = False | |
| for title, ps in top: | |
| raw = _extract(title, intro) | |
| if re.search(r"\b(may|can) refer to\b", raw[:400]): | |
| continue | |
| body = clean(HEADING.sub(" ", raw)) | |
| sents = [s.strip() for s in SENT.split(body) if 20 < len(s.strip()) < 600] | |
| for i, s in enumerate(sents[: 14 if intro else 90]): | |
| sc = ps + sum(1.0 for k in qk if k in s.lower()) + _kind(q, s) + (0.5 if i < 3 else 0.0) | |
| if sc > best[0]: | |
| best = (sc, s, title) | |
| found_kind = _hard(q, s) > 0 | |
| if best[1] and (not typed or found_kind): | |
| break # the intro held an answer of the right kind | |
| return best[1], best[2] | |
| # ------------------------------------------------------------------ the loop | |
| def ask(message: str, tools: bool = True, show: bool = False) -> str: | |
| convo = f"<tools:{'on' if tools else 'off'}>\n<user>\n{message.strip()}\n<|eot|>\n<loom>\n" | |
| first = loom(convo) | |
| m = LOOKUP.search(first) | |
| if not m: | |
| return first | |
| query = m.group(1).strip() | |
| try: | |
| result, source = find(query, message) | |
| except Exception as e: | |
| # Never feed an error in as if it were a result — the model will answer from it. | |
| return f"[harness] lookup failed for {query!r}: {e}" | |
| if not result: | |
| return f"[harness] nothing found for {query!r}" | |
| if show: | |
| print(f" [searched: {query!r}]\n [read from {source}: {result[:150]}]") | |
| return loom(convo + first + f"<|eot|>\n<result>\n{result}\n<|eot|>\n<loom>\n", n=48) | |
| def main() -> int: | |
| global MODEL | |
| ap = argparse.ArgumentParser(description="Loom Spark 3 harness") | |
| ap.add_argument("message", nargs="*") | |
| ap.add_argument("--no-tools", action="store_true", help="chat only, no lookups") | |
| ap.add_argument("--show", action="store_true", help="print the query and the sentence read") | |
| ap.add_argument("--model", default=MODEL) | |
| a = ap.parse_args() | |
| MODEL = a.model | |
| if a.message: | |
| print(ask(" ".join(a.message), not a.no_tools, a.show)); return 0 | |
| print(f"Loom harness — {MODEL} (tools {'off' if a.no_tools else 'on'}, ctrl-c to quit)\n") | |
| while True: | |
| try: | |
| msg = input("you > ").strip() | |
| except (EOFError, KeyboardInterrupt): | |
| print(); return 0 | |
| if msg: | |
| print(f"loom > {ask(msg, not a.no_tools, a.show)}\n") | |
| if __name__ == "__main__": | |
| sys.exit(main()) | |