Text Generation
Transformers
Safetensors
qwen3
generative-retrieval
semantic-id
navigational-search
web-search
conversational
text-generation-inference
Instructions to use Octen/Octen-WebGR-4B with libraries, inference providers, notebooks, and local apps. Follow these links to get started.
- Libraries
- Transformers
How to use Octen/Octen-WebGR-4B with Transformers:
# Use a pipeline as a high-level helper from transformers import pipeline pipe = pipeline("text-generation", model="Octen/Octen-WebGR-4B") messages = [ {"role": "user", "content": "Who are you?"}, ] pipe(messages)# pip install -U transformers accelerate # Load model directly from transformers import AutoTokenizer, AutoModelForCausalLM tokenizer = AutoTokenizer.from_pretrained("Octen/Octen-WebGR-4B") model = AutoModelForCausalLM.from_pretrained("Octen/Octen-WebGR-4B", device_map="auto") messages = [ {"role": "user", "content": "Who are you?"}, ] inputs = tokenizer.apply_chat_template( messages, add_generation_prompt=True, tokenize=True, return_dict=True, return_tensors="pt", ).to(model.device) outputs = model.generate(**inputs, max_new_tokens=256) print(tokenizer.decode(outputs[0][inputs["input_ids"].shape[-1]:])) - Notebooks
- Google Colab
- Kaggle
- Local Apps Settings
- vLLM
How to use Octen/Octen-WebGR-4B with vLLM:
Install from pip and serve model
# Install vLLM from pip: pip install vllm # Start the vLLM server: vllm serve "Octen/Octen-WebGR-4B" # Call the server using curl (OpenAI-compatible API): curl -X POST "http://localhost:8000/v1/chat/completions" \ -H "Content-Type: application/json" \ --data '{ "model": "Octen/Octen-WebGR-4B", "messages": [ { "role": "user", "content": "What is the capital of France?" } ] }'Use Docker
docker model run hf.co/Octen/Octen-WebGR-4B
- SGLang
How to use Octen/Octen-WebGR-4B 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 "Octen/Octen-WebGR-4B" \ --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": "Octen/Octen-WebGR-4B", "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 "Octen/Octen-WebGR-4B" \ --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": "Octen/Octen-WebGR-4B", "messages": [ { "role": "user", "content": "What is the capital of France?" } ] }' - Docker Model Runner
How to use Octen/Octen-WebGR-4B with Docker Model Runner:
docker model run hf.co/Octen/Octen-WebGR-4B
Download infer_gpu.py from Octen/Octen-WebGR-4B: direct link, hf CLI and curl.
- Browser
- Download file 8.98 kB
-
https://huggingface.co/Octen/Octen-WebGR-4B/resolve/main/infer_gpu.py
- Command line
-
hf download hf://Octen/Octen-WebGR-4B/infer_gpu.py
-
curl -L -o infer_gpu.py https://huggingface.co/Octen/Octen-WebGR-4B/resolve/main/infer_gpu.py
8.98 kB
| # Copyright 2026 Octen. Licensed under the Apache License, Version 2.0 (see LICENSE-CODE). | |
| """Reference GPU inference for Octen-WebGR: query -> top-K URLs by constrained beam search over two semantic-ID tokens. | |
| Inference protocol (identical to the evaluation in the technical report): | |
| 1. Normalise the query: collapse Unicode whitespace to one space, strip, lowercase. | |
| 2. Keep at most the first 32 tokens of the query (decoded back to a string before building the prompt). | |
| 3. Prompt = "Query: {q}\\nURL:", tokenised without special tokens. | |
| 4. Level t (t = 0, 1) of the SID is vocabulary token 151936 + t * 65536 + code; only those 65,536 rows of | |
| lm_head are scored at each level. | |
| 5. Constrained beam search: only prefixes present in sid_table/ are allowed, and log_softmax is taken over the | |
| valid children only. K beams are kept at each level (default 20). | |
| 6. Score = sum of the two log-probabilities (exp(score) = the model's probability of the URL among valid SIDs). | |
| (sid0, sid1) is looked up in sid_table/ to get the URL. | |
| This implementation recomputes the prompt for every beam at level 1 and pads each batch to its longest prompt; | |
| it favours clarity over speed. infer_cpu.py reuses the prompt's KV cache instead. | |
| python infer_gpu.py --model Octen/Octen-WebGR-4B --query "youtube" "boeing internship" | |
| python infer_gpu.py --model /path/to/local/copy --check # compare against smoke_test.json | |
| """ | |
| import argparse, glob, json, os, time | |
| import numpy as np | |
| import pyarrow as pa | |
| import pyarrow.parquet as pq | |
| import torch | |
| from transformers import AutoModelForCausalLM, AutoTokenizer | |
| BASE_V, V, L, MAX_Q = 151936, 65536, 2, 32 | |
| PAD = 151643 # <|endoftext|>, used only for left padding (masked out) | |
| def resolve(model): | |
| """Local directory, or a Hugging Face repo id that is downloaded once into the HF cache.""" | |
| if os.path.isdir(model): | |
| return model | |
| from huggingface_hub import snapshot_download | |
| return snapshot_download(model) | |
| def norm_query(q): | |
| return " ".join(q.split()).lower() | |
| class URLs: | |
| """URL column kept as one array per table file (no combined copy, so peak memory is one table); rows are global.""" | |
| def __init__(self, files): | |
| self.parts = [pq.read_table(f, columns=["url"]).column("url").combine_chunks() for f in files] | |
| self.start = np.cumsum([0] + [len(p) for p in self.parts]) | |
| def take(self, rows): | |
| f = np.searchsorted(self.start, rows, side="right") - 1 | |
| return [self.parts[i][int(r - self.start[i])].as_py() for i, r in zip(f, rows)] | |
| class GR: | |
| def __init__(self, model, device="cuda"): | |
| self.root = resolve(model) | |
| self.dev = device | |
| self.dev_type = torch.device(device).type | |
| self.tok = AutoTokenizer.from_pretrained(self.root) | |
| attn = "sdpa" | |
| if self.dev_type == "cuda": | |
| try: | |
| import flash_attn # noqa: F401 | |
| attn = "flash_attention_2" | |
| except ImportError: | |
| pass | |
| self.lm = AutoModelForCausalLM.from_pretrained(self.root, dtype=torch.bfloat16, attn_implementation=attn).to(device).eval() | |
| assert self.lm.config.vocab_size == BASE_V + L * V, "unexpected vocabulary size: should be 151936 + 2 x 65536" | |
| self.W = self.lm.lm_head.weight | |
| files = sorted(glob.glob(f"{self.root}/sid_table/part-*.parquet")) | |
| t = pa.concat_tables([pq.read_table(f, columns=["sid0", "sid1"]) for f in files]) | |
| key = t.column("sid0").to_numpy().astype(np.int64) * V + t.column("sid1").to_numpy().astype(np.int64) | |
| assert (np.diff(key) > 0).all(), "sid_table must be sorted by key in strictly ascending order (one URL per SID)" | |
| self.keys = torch.from_numpy(key).to(device) # row number = position in keys | |
| ok0 = np.zeros(V, bool) | |
| ok0[np.unique(key // V)] = True | |
| self.ok0 = torch.from_numpy(ok0).to(device) # valid level-0 codes | |
| self.url = URLs(files) | |
| self.n = len(key) | |
| def prompts(self, qs): | |
| qs = [norm_query(q) for q in qs] | |
| qi = self.tok(qs, add_special_tokens=False)["input_ids"] | |
| qs = [self.tok.decode(x[:MAX_Q]) if len(x) > MAX_Q else q for q, x in zip(qs, qi)] | |
| return self.tok([f"Query: {q}\nURL:" for q in qs], add_special_tokens=False)["input_ids"] | |
| def last_hidden(self, seqs): | |
| P = max(len(s) for s in seqs) | |
| ids = torch.full((len(seqs), P), PAD, dtype=torch.long) | |
| am = torch.zeros((len(seqs), P), dtype=torch.long) | |
| for i, s in enumerate(seqs): # left padding: the last position is each sequence's last token | |
| ids[i, P - len(s):] = torch.tensor(s) | |
| am[i, P - len(s):] = 1 | |
| ids, am = ids.to(self.dev), am.to(self.dev) | |
| pos = (am.cumsum(1) - 1).clamp(min=0) | |
| return self.lm.model(input_ids=ids, attention_mask=am, position_ids=pos).last_hidden_state[:, -1] | |
| def search(self, qs, K=20): | |
| """-> one list per query of (url, log_prob, table_row), sorted by score.""" | |
| with torch.autocast(self.dev_type, dtype=torch.bfloat16): | |
| P = self.prompts(qs) | |
| B = len(P) | |
| ninf = float("-inf") | |
| lg0 = (self.last_hidden(P) @ self.W[BASE_V:BASE_V + V].t()).float() # [B, V] | |
| lp0 = lg0.masked_fill(~self.ok0, ninf).log_softmax(-1).masked_fill(~self.ok0, ninf) | |
| k0 = min(K, int(self.ok0.sum())) | |
| s0, c0 = lp0.topk(k0, dim=1) # [B, k0] | |
| # Level 1: one sequence per (query, level-0 candidate) = prompt + that SID token | |
| P1 = [p + [BASE_V + c] for p, cs in zip(P, c0.tolist()) for c in cs] | |
| lg1 = (self.last_hidden(P1) @ self.W[BASE_V + V:BASE_V + 2 * V].t()).float() # [B*k0, V] | |
| cc = c0.reshape(-1) | |
| lo = torch.searchsorted(self.keys, cc * V) | |
| n = torch.searchsorted(self.keys, (cc + 1) * V) - lo # number of valid children per prefix | |
| r = torch.repeat_interleave(torch.arange(len(cc), device=self.dev), n) | |
| j = torch.arange(int(n.sum()), device=self.dev) - torch.repeat_interleave(n.cumsum(0) - n, n) + lo[r] | |
| ok1 = torch.zeros_like(lg1, dtype=torch.bool) | |
| ok1[r, self.keys[j] - cc[r] * V] = True | |
| lp1 = lg1.masked_fill(~ok1, ninf).log_softmax(-1).masked_fill(~ok1, ninf) | |
| cand = (s0.reshape(-1, 1) + lp1).view(B, k0 * V) | |
| s, flat = cand.topk(min(K, k0 * V), dim=1) | |
| key = c0.gather(1, flat // V) * V + flat % V | |
| row = torch.searchsorted(self.keys, key) | |
| s, row = s.float().cpu().numpy(), row.cpu().numpy() | |
| out = [] | |
| for b in range(B): | |
| m = np.isfinite(s[b]) | |
| rr = row[b][m] | |
| out.append(list(zip(self.url.take(rr), s[b][m].tolist(), rr.tolist()))) | |
| return out | |
| def check(gr, bs=32): | |
| """Compare against smoke_test.json (hand-picked queries; expected results from the released weights and table).""" | |
| ref = json.load(open(f"{gr.root}/smoke_test.json")) | |
| assert gr.n == ref["table_rows"], f"sid_table has {gr.n:,} rows, expected {ref['table_rows']:,}" | |
| qs, want = [x["query"] for x in ref["cases"]], [x["top20"] for x in ref["cases"]] | |
| ov, top1, n = [], 0, len(qs) | |
| for i in range(0, n, bs): | |
| for res, w in zip(gr.search(qs[i:i + bs], 20), want[i:i + bs]): | |
| got = [u for u, *_ in res] | |
| ov.append(len(set(got) & set(w)) / max(len(w), 1)) | |
| top1 += bool(got and w and got[0] == w[0]) | |
| mo, t1 = float(np.mean(ov)), top1 / n | |
| print(f"{n} queries: mean top-20 overlap {mo:.4f}, top-1 agreement {t1:.4f}, identical top-20 {np.mean(np.array(ov) == 1):.3f}") | |
| ok = mo >= 0.9 and t1 >= 0.9 | |
| print("PASS" if ok else "FAIL (small differences from batching and kernels are expected, but overlap should be >= 0.9)") | |
| return ok | |
| def main(): | |
| ap = argparse.ArgumentParser() | |
| ap.add_argument("--model", default=os.path.dirname(os.path.abspath(__file__)), help="local directory or HF repo id") | |
| ap.add_argument("--query", nargs="*", default=[]) | |
| ap.add_argument("--k", type=int, default=20) | |
| ap.add_argument("--device", default="cuda") | |
| ap.add_argument("--check", action="store_true") | |
| a = ap.parse_args() | |
| t0 = time.time() | |
| gr = GR(a.model, a.device) | |
| print(f"Loaded {gr.n:,} candidate URLs in {time.time() - t0:.0f}s", flush=True) | |
| for q in a.query: | |
| t = time.perf_counter() | |
| res = gr.search([q], a.k)[0] | |
| print(f"\n{q} ({(time.perf_counter() - t) * 1000:.0f} ms)") | |
| for i, (u, lp, _) in enumerate(res, 1): | |
| print(f" {i:2d}. {np.exp(lp):.3f} {u}") | |
| if a.check and not check(gr): | |
| raise SystemExit(1) | |
| if __name__ == "__main__": | |
| main() | |