- Granite 4.2 8B is a free model from IBM that you can run on your own computer. In our tests it's decent, with trade-offs: 77 out of 100, #15 of 56.
- It solved 23 of 30 coding jobs and scored 78 on reading documents. On our hardest tasks it scored 55.
- Runs on an 8 GB graphics card or a Mac with 16 GB.
Coding
Our coding test is 30 programming jobs, from small ones like reading time durations or cleaning up messy data to harder ones like a config-file parser or a double-entry ledger. We run each answer against tests the model never sees, and a job only counts if everything passes. Granite 4.2 8B got 23 of 30 right. The best local coders solved 29 of 30.
Reading documents
The second test hands the model things like an expense claim thread, a pay stub or an insurance statement, and asks for specific numbers and dates. Many questions need a bit of math, or noticing a correction further down the email. Granite 4.2 8B scored 78; the best model scored 100.
| Test | Score | Public questions | Secret questions |
|---|---|---|---|
| Coding | 77 | 86 | 74 |
| Reading documents | 78 | 71 | 80 |
| Decisions | 93 | 96 | 92 |
On the 18 hardest tasks (included in the scores above) it scored 55. This number separates the top models.
We tested the full-size version online. The smaller versions you download usually score a little lower.
Can your computer run it?
You can download Granite 4.2 8B in several sizes. Smaller versions fit on more computers and lose a little quality, so pick the biggest one that fits.
| Your computer | Runs it? | Version to download |
|---|---|---|
| Graphics card, 8 GB | Yes | Q4 |
| Graphics card, 12 GB | Yes | Q8 |
| Graphics card, 16 GB | Yes | Q8 |
| Graphics card, 24 GB | Yes | Q8 |
| Graphics card, 32 GB | Yes | Q8 |
| Mac, 16 GB | Yes | Q6 |
| Mac, 24 GB | Yes | Q8 |
| Mac, 32 GB | Yes | Q8 |
| Mac, 48 GB | Yes | Q8 |
| Mac, 64 GB | Yes | Q8 |
| Mac, 96 GB | Yes | Q8 |
| Mac, 128 GB | Yes | Q8 |
For normal-length chats. Long documents need more memory. Q4 or Q8? · How we worked this out
How to try it
Search for "Granite 4.2 8B GGUF" in LM Studio or on Hugging Face and pick the version that fits your computer. New to this? Start with our Ollama guide.
Want the strongest all-rounder instead? Qwen3.6 27B scored 97.
Its actual answers
What Granite 4.2 8B wrote for our public questions, word for word.
Coding: INI parser (hard) Didn't work
```python
import re
from typing import Dict, Any
def parse_ini(text: str) -> dict[str, dict[str, str]]:
lines = text.splitlines()
raw_sections: Dict[str, Dict[str, str]] = {}
current_section = None
pending_key = None
pending_value = None
def strip_comment(line: str) -> str:
"""Remove inline comment if present, respecting quotes."""
inside_quote = False
for i, ch in enumerate(line):
if ch == '"':
if not inside_quote:
inside_quote = True
else:
inside_quote = False
elif ch in ';#' and not inside_quote:
if i > 0 and line[i-1].isspace():
return line[:i] # cut before the comment marker
return line
for raw_line in lines:
# Rule 3: ignore empty or comment-start lines
stripped_raw = raw_line.lstrip()
if not stripped_raw or stripped_raw[0] in ';#':
continue
clean_line = strip_comment(raw_line)
# Check for section header first
if clean_line.startswith('[') and clean_line.endswith(']'):
# finalize pending key if any
if pending_key is not None and current_section is not None:
if current_section not in raw_sections:
raw_sections[current_section] = {}
raw_sections[current_section][pending_key] = pending_value
pending_key = None
pending_value = None
# section name (do not trim inside)
name = clean_line[1:-1]
current_section = name
if current_section not in raw_sections:
raw_sections[current_section] = {}
continue
# Check for continuation line
if pending_key is not None and current_section is not None and clean_line.startswith((' ', '\t')):
# continuation: append stripped text with newline
stripped_text = clean_line.strip()
pending_value += "\n" + stripped_text
continue
# Otherwise, key line (must be inside a section)
if current_section is None:
# key line before any section -> ValueError
raise ValueError("Key line before any section header")
# Find first '=' or ':' outside quotes
inside_quote = False
found_sep = None
sep_char = None
for i, ch in enumerate(clean_line):
if ch == '"':
if not inside_quote:
inside_quote = True
else:
inside_quote = False
elif ch in '=:' and not inside_quote:
if found_sep is None:
found_sep = i
sep_char = ch
if found_sep is None:
# malformed key line
raise ValueError("Malformed key line: missing separator outside quotes")
key_part = clean_line[:found_sep]
value_part = clean_line[found_sep+1:]
key_raw = key_part.strip()
if not key_raw:
raise ValueError("Empty key")
key = key_raw.lower()
# Process value part (remove surrounding quotes)
val = value_part.strip()
if val.startswith('"') and val.endswith('"') and len(val) >= 2:
val = val[1:-1]
# Finalize previous pending key if exists
if pending_key is not None:
if current_section not in raw_sections:
raw_sections[current_section] = {}
raw_sections[current_section][pending_key] = pending_value
pending_key = None
pending_value = None
# Start new pending key
pending_key = key
pending_value = val
# Finalize pending key after loop
if pending_key is not None and current_section is not None:
if current_section not in raw_sections:
raw_sections[current_section] = {}
raw_sections[current_section][pending_key] = pending_value
# Interpolation resolution
cache: Dict[tuple[str, str], str] = {}
visiting: set[tuple[str, str]] = set()
def substitute(value: str, section_name: str) -> str:
"""Replace ${...} placeholders in value that are outside quotes."""
# simple scan, assuming no quotes inside value after quote removal
# (spec says quoted values keep everything inside quotes exactly,
# so we do not substitute inside quotes; but we have already removed
# outer quotes, so remaining value should not contain quotes that delimit)
i = 0
n = len(value)
out = []
while i < n:
if value[i] == '$' and i + 1 < n and value[i+1] == '{':
j = i + 2
while j < n and value[j] != '}':
j += 1
if j == n:
raise KeyError(f"Unterminated placeholder at position {i}")
content = value[i+2:j]
i = j + 1
# parse content
if ':' in content:
# ${section:key}
parts = content.split(':', 1)
ref_section = parts[0]
ref_key = parts[1]
else:
# ${key}
ref_section = None
ref_key = content
try:
if ref_section is None:
# same section: look in own dict first, then DEFAULT
if ref_key in raw_sections[section_name]:
res_section = section_name
res_key = ref_key
elif "DEFAULT" in raw_sections and ref_key in raw_sections["DEFAULT"]:
res_section = "DEFAULT"
res_key = ref_key
else:
raise KeyError(f"Key {ref_key} not found in section {section_name} or DEFAULT")
else:
# other section: must exist and key must be in that section
if ref_section not in raw_sections:
raise KeyError(f"Section {ref_section} not defined")
if ref_key in raw_sections[ref_section]:
res_section = ref_section
res_key = ref_key
else:
raise KeyError(f"Key {ref_key} not found in section {ref_section}")
except KeyError as e:
raise KeyError(str(e))
# resolve recursively
try:
resolved_ref = resolve_key(res_section, res_key)
except ValueError as ve:
raise ValueError(str(ve))
except KeyError as ke:
raise ke
out.append(resolved_ref)
else:
out.append(value[i])
i += 1
return ''.join(out)
def resolve_key(section_name: str, key: str) -> str:
key_pair = (section_name, key)
if key_pair in cache:
return cache[key_pair]
if key_pair in visiting:
raise ValueError("Interpolation cycle detected")
visiting.add(key_pair)
raw_val = raw_sections[section_name][key]
resolved_val = substitute(raw_val, section_name)
visiting.remove(key_pair)
cache[key_pair] = resolved_val
return resolved_val
# Resolve all keys (including DEFAULT)
for sec, sec_dict in raw_sections.items():
for key in sec_dict:
try:
resolve_key(sec, key)
except (KeyError, ValueError) as e:
# re-raise to propagate
raise e
# Build output excluding DEFAULT
output: Dict[str, Dict[str, str]] = {}
for sec, sec_dict in raw_sections.items():
if sec == "DEFAULT":
continue
out_dict = {}
for key in sec_dict:
# retrieve resolved value from cache
resolved = cache[(sec, key)]
out_dict[key] = resolved
output[sec] = out_dict
return output
``` Coding: TTL LRU (hard) Worked
```python
import collections
class TTLCache:
def __init__(self, capacity: int, ttl: float, clock):
self.capacity = capacity
self.ttl = ttl
self._clock = clock
self._order = collections.OrderedDict() # keys in recency order, least recent first
self._info = {} # key -> (value, expiry)
self._hits = 0
self._misses = 0
self._evictions = 0
self._expirations = 0
def _now(self):
return self._clock()
def _purge_expired(self):
"""Remove all expired entries and count each removal as an expiration."""
now = self._now()
to_remove = []
for key in self._order:
_, expiry = self._info[key]
if now >= expiry:
to_remove.append(key)
for key in to_remove:
del self._info[key]
self._order.pop(key, None)
self._expirations += 1
def _remove_single_expired(self, key):
"""Remove an expired entry (the key is known to be expired) and count it."""
self._expirations += 1
_, expiry = self._info[key]
del self._info[key]
self._order.pop(key, None)
def put(self, key, value):
if self.capacity == 0:
return
now = self._now()
if key in self._info:
# replacement: refresh expiry and mark most recently used
self._info[key] = (value, now + self.ttl)
self._order.move_to_end(key)
else:
# new key: first purge all expired entries
self._purge_expired()
# if cache is full (non‑expired entries == capacity), evict LRU
if len(self._info) >= self.capacity:
lru_key, _ = self._order.popitem(last=False)
del self._info[lru_key]
self._evictions += 1
# insert the new entry
self._info[key] = (value, now + self.ttl)
self._order[key] = None
def get(self, key, default=None):
if self.capacity == 0:
self._misses += 1
return default
now = self._now()
if key not in self._info:
self._misses += 1
return default
value, expiry = self._info[key]
if now >= expiry:
# expired entry: remove it (counts as expiration) and this get is a miss
self._remove_single_expired(key)
self._misses += 1
return default
else:
# non‑expired: move to most recent and count as hit
self._order.move_to_end(key)
self._hits += 1
return value
def __len__(self):
if self.capacity == 0:
return 0
now = self._now()
cnt = 0
for _, expiry in self._info.values():
if now < expiry:
cnt += 1
return cnt
def stats(self):
return {
"hits": self._hits,
"misses": self._misses,
"evictions": self._evictions,
"expirations": self._expirations
}
``` Coding: Parse duration Worked
```python
def parse_duration(s: str) -> int:
if not s.strip():
raise ValueError()
n = len(s)
i = 0
last_unit_num = None
used_units = set()
parts = []
while i < n:
# Skip whitespace
while i < n and s[i].isspace():
i += 1
if i == n:
break
if s[i] in '0123456789':
j = i
while j < n and s[j] in '0123456789':
j += 1
num_str = s[i:j]
val = int(num_str)
if j == n:
# Number without unit
raise ValueError()
ch = s[j]
unit_lower = ch.lower()
if unit_lower not in ('h', 'm', 's'):
raise ValueError()
if unit_lower in used_units:
raise ValueError()
unit_num = {'h': 0, 'm': 1, 's': 2}[unit_lower]
if last_unit_num is not None and unit_num < last_unit_num:
raise ValueError()
parts.append((val, unit_num))
used_units.add(unit_lower)
last_unit_num = unit_num
i = j + 1
else:
raise ValueError()
total = 0
for val, unit_num in parts:
if unit_num == 0:
total += val * 3600
elif unit_num == 1:
total += val * 60
else:
total += val
return total
``` Coding: Summarize ranges Worked
```python
def summarize_ranges(nums: list[int]) -> str:
uniq = sorted(set(nums))
parts = []
i = 0
n = len(uniq)
while i < n:
j = i
while j + 1 < n and uniq[j + 1] == uniq[j] + 1:
j += 1
if j - i + 1 >= 3:
parts.append(f"{uniq[i]}..{uniq[j]}")
else:
for k in range(i, j + 1):
parts.append(str(uniq[k]))
i = j + 1
return ",".join(parts)
``` Coding: Top customers Worked
```python
def top_customers(rows: list[dict], n: int) -> list[tuple[str, float]]:
customers = {} # normalized -> (representative_name, running_total)
for row in rows:
cust = row.get("customer", "").strip()
if not cust:
continue
amt_str = row.get("amount", "").strip()
clean_amt = amt_str.replace('$', '').replace(',', '')
try:
amount = float(clean_amt)
except ValueError:
continue
norm = cust.casefold()
if norm in customers:
name, running = customers[norm]
customers[norm] = (name, running + amount)
else:
customers[norm] = (cust, amount)
# Round totals
results = []
for norm, (name, running_total) in customers.items():
total = round(running_total, 2)
results.append((name, total))
# Sort by total descending, then name ascending
results.sort(key=lambda x: (-x[1], x[0]))
return results[:n]
``` Coding: Slugify Worked
```python
import unicodedata
def slugify(title: str, max_len: int = 50) -> str:
# Step 1: transliterate accented characters to ASCII, drop other non-ASCII
normalized = unicodedata.normalize('NFKD', title)
ascii_str = ''.join(ch for ch in normalized if ord(ch) < 128)
# Step 2: lowercase
lowered = ascii_str.lower()
# Step 3: extract words (a-z and 0-9) and join with hyphens
words = []
cur = []
for ch in lowered:
if ('a' <= ch <= 'z') or ('0' <= ch <= '9'):
cur.append(ch)
else:
if cur:
words.append(''.join(cur))
cur = []
if cur:
words.append(''.join(cur))
slug = '-'.join(words)
# Already within limit?
if len(slug) <= max_len:
return slug
# Step 4: truncate without cutting words
words_slug = slug.split('-')
first_len = len(words_slug[0])
if first_len > max_len:
# hard‑cut the first word
return words_slug[0][:max_len]
accumulated = []
current_len = 0
for w in words_slug:
if not accumulated:
accumulated.append(w)
current_len = len(w)
else:
if current_len + 1 + len(w) <= max_len:
accumulated.append(w)
current_len += 1 + len(w)
else:
break
return '-'.join(accumulated)
``` Coding: Token bucket Worked
```python
class TokenBucket:
def __init__(self, capacity: float, refill_per_sec: float, clock):
self._capacity = capacity
self._refill_per_sec = refill_per_sec
self._clock = clock
self._tokens = float(capacity)
self._last_time = self._clock()
def allow(self, cost: float = 1.0) -> bool:
now = self._clock()
elapsed = now - self._last_time
self._tokens = min(self._capacity, self._tokens + self._refill_per_sec * elapsed)
self._last_time = now
if self._tokens >= cost:
self._tokens -= cost
return True
return False
@property
def tokens(self) -> float:
now = self._clock()
elapsed = now - self._last_time
self._tokens = min(self._capacity, self._tokens + self._refill_per_sec * elapsed)
self._last_time = now
return self._tokens
``` Decisions: Refund window (hard) 100% right
{
"outcome": {
"full_refund": 0.0,
"partial_refund": 0.0,
"store_credit": 1.0,
"nothing": 0.0
},
"defective": {
"true": 0.0,
"false": 1.0
}
} Decisions: Incident matrix (hard) 100% right
{
"severity": {
"0": 0.0,
"1": 1.0,
"2": 0.0,
"3": 0.0
},
"page": {
"true": 0.0,
"false": 1.0
}
} Decisions: Tool followup (hard) 50% right
{
"tool": {
"web_search": 0.01,
"calculator": 0.03,
"calendar": 0.9,
"email": 0.01,
"none": 0.05
},
"confirm": {
"true": 0.1,
"false": 0.9
}
} Decisions: Legit security alert (hard) 100% right
{
"phishing": { "true": 0.02, "false": 0.98 },
"action_needed": { "true": 0.05, "false": 0.95 }
} Decisions: Meeting slot (hard) 100% right
{
"slot": {
"A": 0.0,
"B": 1.0,
"C": 0.0,
"D": 0.0
},
"raj_last": {
"true": 1.0,
"false": 0.0
}
} Decisions: Review mixed (hard) 100% right
{"hardware": {"true": 0.99, "false": 0.01}, "support": {"true": 0.99, "false": 0.01}} Decisions: Support checkout down 100% right
{
"department": {
"billing": 0.2,
"technical": 0.6,
"account": 0.1,
"shipping": 0.05,
"sales": 0.05
},
"urgency": {
"0": 0.02,
"1": 0.03,
"2": 0.05,
"3": 0.9
},
"outage": {
"true": 0.99,
"false": 0.01
}
} Decisions: Refund wrong plan 100% right
{
"department": {
"billing": 1.0,
"technical": 0.0,
"account": 0.0,
"shipping": 0.0,
"sales": 0.0
},
"refund": {
"true": 1.0,
"false": 0.0
},
"tone": {
"calm": 1.0,
"frustrated": 0.0
}
} Decisions: Moderation doxxing 100% right
{
"policy": {
"none": 0.0,
"harassment": 1.0,
"hate": 0.0,
"spam": 0.0,
"self_harm": 0.0
},
"personal_info": {
"true": 1.0,
"false": 0.0
}
} Decisions: Route calendar 100% right
{
"tool": {
"web_search": 0.0,
"calculator": 0.0,
"calendar": 1.0,
"email": 0.0,
"none": 0.0
},
"confirm": {
"true": 1.0,
"false": 0.0
}
} Decisions: Doc invoice missing due 100% right
{
"doc_type": {
"invoice": 1.0,
"resume": 0.0,
"contract": 0.0,
"bank_statement": 0.0,
"other": 0.0
},
"missing_due_date": {
"true": 1.0,
"false": 0.0
}
} Decisions: Phishing paypal 100% right
{
"phishing": {
"true": 1.0,
"false": 0.0
},
"risk": {
"0": 0.0,
"1": 0.0,
"2": 0.0,
"3": 1.0
}
} Decisions: Pii ssn email 100% right
{
"data_kind": {
"none": 0.0,
"contact": 0.0,
"financial": 0.0,
"government_id": 1.0,
"health": 0.0
},
"sensitive": {
"true": 1.0,
"false": 0.0
}
} Decisions: Review mixed 100% right
{"sentiment": {"positive": 0.05, "neutral": 0.1, "negative": 0.85}, "defect": {"true": 0.95, "false": 0.05}, "recommend": {"true": 0.05, "false": 0.95}} Documents: Saas escalator (hard) 70% right
{
"year2_price_per_seat_month": 47.25,
"year3_price_per_seat_month": 47.25,
"year1_invoice": 58200.00,
"year2_invoice": 61236.00,
"addon_months_billed": 6,
"addon_invoice": 67473.00,
"year3_invoice": 134946.00,
"year3_discount_percent": 15.00,
"total_contract_value": 321855.00,
"contract_end_date": "2027-02-28"
} Documents: Expense thread 100% right
{
"employee_id": "EMP-20417",
"destination_city": "Lisbon",
"trip_start": "2025-02-24",
"trip_end": "2025-02-27",
"approved_items": [
{"date": "2025-02-24", "category": "airfare", "amount_usd": 1184.60},
{"date": "2025-02-24", "category": "ground_transport", "amount_usd": 38.88},
{"date": "2025-02-25", "category": "meals", "amount_usd": 229.39},
{"date": "2025-02-26", "category": "lodging", "amount_usd": 466.56},
{"date": "2025-02-27", "category": "ground_transport", "amount_usd": 44.82}
],
"rejected_item_count": 1,
"per_diem_days": 3,
"per_diem_usd": 195.00,
"total_reimbursable_usd": 2159.25,
"approver_email": "priya.raman@corvane.com"
} Documents: Lease amendment 92% right
{
"tenants": ["Marcus Lin", "Sofia Lin"],
"landlord": "Ridgeline Property Group LLC",
"zip": "97205",
"lease_end": "2025-11-30",
"original_monthly_rent": 2150,
"monthly_rent_from_2025_06_01": 2236,
"late_fee_from_2025_06_01": 111.8,
"security_deposit": 2150,
"total_pet_deposits": 800,
"total_monthly_payment_july_2025": 2306,
"move_in_payment": 5750
} Documents: Ticket SLA 92% right
{
"ticket_id": "48213",
"account_id": "ACC-7731",
"open_issue": "inventory_sync",
"resolved_issues": ["billing_address", "invoice_pdf"],
"affected_orders": ["SO-99812", "SO-99820", "SO-99827"],
"priority": "P2",
"sla_due_local": "2025-09-15T15:30",
"sla_due_utc": "2025-09-15T20:30:00Z",
"reissued_invoice": "INV-2025-0812"
} Documents: Sales footnotes 0% right
Size: 8.8B parameters. First tested OCT 10.
Comments
Sign in with GitHub to comment. Spam and abuse are hidden automatically.