- Granite 4.0 Micro (Q4, Mac) is a free model from IBM that you can run on your own computer. In our tests it's not one we'd recommend right now: 22 out of 100, #49 of 56.
- It solved 4 of 30 coding jobs and scored 31 on reading documents. On our hardest tasks it scored 14.
- 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.0 Micro (Q4, Mac) got 4 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.0 Micro (Q4, Mac) scored 31; the best model scored 100.
| Test | Score | Public questions | Secret questions |
|---|---|---|---|
| Coding | 13 | 0 | 17 |
| Reading documents | 31 | 41 | 28 |
| Decisions | ? | ? | ? |
On the 18 hardest tasks (included in the scores above) it scored 14. This number separates the top models.
We tested the Q4 download, the version most people run, with Ollama on our Mac mini (M4, 16 GB).
Can your computer run it?
Granite 4.0 Micro (Q4, Mac) comes in one standard size, which needs about 3.2 GB of memory.
| Your computer | Runs it? | Version to download |
|---|---|---|
| Graphics card, 8 GB | Yes | Q4 |
| Graphics card, 12 GB | Yes | Q4 |
| Graphics card, 16 GB | Yes | Q4 |
| Graphics card, 24 GB | Yes | Q4 |
| Graphics card, 32 GB | Yes | Q4 |
| Mac, 16 GB | Yes | Q4 |
| Mac, 24 GB | Yes | Q4 |
| Mac, 32 GB | Yes | Q4 |
| Mac, 48 GB | Yes | Q4 |
| Mac, 64 GB | Yes | Q4 |
| Mac, 96 GB | Yes | Q4 |
| Mac, 128 GB | Yes | Q4 |
For normal-length chats. Long documents need more memory. Q4 or Q8? · How we worked this out
How to try it
The easiest way is Ollama. Install it, then run:
ollama run granite4:micro-h Want the strongest all-rounder instead? Qwen3.6 27B scored 97.
Its actual answers
What Granite 4.0 Micro (Q4, Mac) wrote for our public questions, word for word.
Coding: INI parser (hard) Didn't work
```python
import re
from typing import Dict
def parse_ini(text: str) -> Dict[str, Dict[str, str]]:
def resolve_key(section: str, key: str) -> str:
parts = key.split(':', 1)
if len(parts) == 1:
return sections.get(section, {}).get(key)
else:
sec, k = parts
return resolve_key(sec, k)
lines = text.splitlines()
sections = {}
current_section = 'DEFAULT'
result = {}
key_pattern = re.compile(r'^\s*([\w-]+)\s*(?:=[^=]+|:)\s*$', re.IGNORECASE)
comment_pattern = re.compile(r'^\s*;.*|^\s*#\s*.*$', re.IGNORECASE)
i = 0
while i < len(lines):
line = lines[i].strip()
if not line or comment_pattern.match(line):
i += 1
continue
if line.startswith('[') and line.endswith(']'):
sec_name = line[1:-1]
if sec_name == '':
raise ValueError("Empty section name")
current_section = sec_name
result[current_section] = {}
i += 1
continue
match = key_pattern.match(line)
if not match:
raise ValueError("Malformed line")
key, raw_value = match.groups()
value = raw_value[1:].rstrip() if value[0] in ('"', "'") else raw_value
if current_section not in result:
result[current_section] = {}
result[current_section][key.lower()] = value
i += 1
# Handle interpolation
def resolve_key_recursive(section: str, key: str) -> str:
nonlocal result
value = resolve_key(section, key)
while '${' in value:
value = re.sub(r'\$\{([^}]+)\}', lambda m: resolve_key_recursive(section, m.group(1)), value)
return value
for sec, keys in result.items():
for k in keys:
result[sec][k] = resolve_key_recursive(sec, k)
return result
``` Coding: TTL LRU (hard) Didn't work
```python
from collections import OrderedDict
from typing import Callable, Dict, Any, Optional
import time
class TTLCache:
def __init__(self, capacity: int, ttl: float, clock: Callable[[], float]):
self.capacity = capacity
self.ttl = ttl
self.clock = clock
self.cache: OrderedDict[Any, Dict[str, Any]] = OrderedDict()
self.stats: Dict[str, int] = {"hits": 0, "misses": 0, "evictions": 0, "expirations": 0}
def _evict_expired(self) -> None:
now = self.clock()
while self.cache and now >= self.cache[next(iter(self.cache))]["expiry"]:
self.cache.popitem(last=False)
self.stats["expirations"] += 1
def put(self, key: Any, value: Any) -> None:
now = self.clock()
if key in self.cache:
self.cache.move_to_end(key)
self.cache[key]["value"] = value
self.cache[key]["expiry"] = now + self.ttl
self.stats["hits"] += 1
else:
if len(self.cache) >= self.capacity:
self._evict_expired()
if len(self.cache) >= self.capacity:
self.stats["evictions"] += 1
self.cache.popitem(last=False)
self.cache[key] = {"value": value, "expiry": now + self.ttl}
self.stats["hits"] += 1
def get(self, key: Any, default: Optional[Any] = None) -> Any:
now = self.clock()
if key not in self.cache:
self.stats["misses"] += 1
return default
if now >= self.cache[key]["expiry"]:
self.cache.pop(key)
self.stats["expirations"] += 1
return default
self.cache.move_to_end(key)
self.stats["hits"] += 1
return self.cache[key]["value"]
def __len__(self) -> int:
self._evict_expired()
return len(self.cache)
def stats(self) -> Dict[str, int]:
return self.stats
``` Coding: Parse duration Didn't work
```python
def parse_duration(s: str) -> int:
"""
Convert a duration string into a total number of seconds.
Supported units: 'h' (hours), 'm' (minutes), 's' (seconds).
Units are case-insensitive. Each part is a non-negative integer
followed by a unit, separated by optional whitespace.
Raises ValueError for:
- Empty/blank strings
- Numbers without a unit
- Unknown units
- Decimals
- Repeated units
- Units out of order (e.g., 'ms')
"""
if not s.strip():
raise ValueError("Empty string provided")
total_seconds = 0
parts = s.split()
seen_units = set()
for part in parts:
# Check for decimals
if '.' in part:
raise ValueError("Decimals are not supported")
# Extract numeric part and unit
num_str = ''
unit = ''
for char in part:
if char.isdigit():
num_str += char
elif char.upper() in 'HMS':
unit += char.upper()
else:
raise ValueError(f"Unknown unit '{char}' in '{part}'")
# Validate parts
if not num_str:
raise ValueError(f"Number missing in '{part}'")
if not unit:
raise ValueError(f"Unit missing in '{part}'")
if unit not in 'HMS':
raise ValueError(f"Unknown unit '{unit}' in '{part}'")
if unit in seen_units:
raise ValueError(f"Duplicate unit '{unit}' in '{part}'")
seen_units.add(unit)
# Convert and accumulate
total_seconds += int(num_str) * {'H': 3600, 'M': 60, 'S': 1}[unit]
return total_seconds
``` Coding: Summarize ranges Didn't work
```python
from typing import List
def summarize_ranges(nums: List[int]) -> str:
if not nums:
return ""
nums = sorted(set(nums))
result = []
i = 0
while i < len(nums):
start = nums[i]
while i + 1 < len(nums) and nums[i] + 1 == nums[i + 1]:
i += 1
end = nums[i]
if end - start >= 2:
result.append(f"{start}..{end}")
else:
result.extend(str(num) for num in nums[start:i+1])
i += 1
return ",".join(result)
``` Coding: Top customers Didn't work
```python
from typing import List, Tuple
from decimal import Decimal
def top_customers(rows: List[dict], n: int) -> List[Tuple[str, float]]:
customer_totals = {}
for row in rows:
customer_name = row.get("customer", "").strip()
amount_str = row.get("amount", "").strip()
if not customer_name or not amount_str:
continue
try:
amount = Decimal(amount_str.replace('$', '').replace(',', ''))
except (ValueError, DecimalException):
continue
customer_name = customer_name.lower()
if customer_name not in customer_totals:
customer_totals[customer_name] = Decimal(0)
customer_totals[customer_name] += amount
sorted_customers = sorted(
customer_totals.items(),
key=lambda x: (-x[1], x[0])
)
return [(name.capitalize(), float(total)) for name, total in sorted_customers[:n]]
``` Coding: Slugify Didn't work
```python
import unicodedata
import re
def slugify(title: str, max_len: int = 50) -> str:
# 1. Transliterate accented characters to ASCII and drop non-ASCII
ascii_title = unicodedata.normalize('NFKD', title).encode('ascii', 'ignore').decode('ascii')
# 2. Lowercase
ascii_title = ascii_title.lower()
# 3. Replace non-alphanumeric with '-', strip leading/trailing '-'
slug = re.sub(r'[^a-z0-9]+', '-', ascii_title).strip('-')
# 4. Shorten if necessary without cutting a word
if len(slug) > max_len:
# Find the last hyphen within the max_len
cut_pos = slug.rfind('-', 0, max_len)
if cut_pos == -1: # If no hyphen found, cut to max_len
slug = slug[:max_len]
else:
slug = slug[:cut_pos]
return slug
``` Coding: Token bucket Didn't work
```python
from typing import Callable
class TokenBucket:
def __init__(self, capacity: float, refill_per_sec: float, clock: Callable[[], float]):
self._capacity = capacity
self._refill_per_sec = refill_per_sec
self._clock = clock
self._tokens = capacity
@property
def tokens(self) -> float:
self._refill()
return self._tokens
def _refill(self) -> None:
elapsed = self._clock() - self._last_refill_time
refill_amount = min(self._refill_per_sec * elapsed, self._capacity - self._tokens)
self._tokens = min(self._capacity, self._tokens + refill_amount)
self._last_refill_time = self._clock()
def allow(self, cost: float = 1) -> bool:
self._refill()
if self._tokens >= cost:
self._tokens -= cost
return True
return False
``` Decisions: Support checkout down 0% right
{
"department": "technical",
"urgency": "3",
"outage": "true"
} Decisions: Refund wrong plan 0% right
{
"department": "billing",
"refund": "true",
"tone": "frustrated"
} Decisions: Moderation doxxing 50% right
{
"policy": {
"none": 0.95,
"harassment": 0.01,
"hate": 0.01,
"spam": 0.01,
"self_harm": 0.01
},
"personal_info": {
"true": 0.95,
"false": 0.05
}
} Decisions: Route calendar 0% right
{
"tool": "calendar",
"confirm": "true"
} Decisions: Doc invoice missing due 50% right
{
"doc_type": {
"invoice": 1
},
"missing_due_date": {
"false": 1
}
} Decisions: Phishing paypal 0% right
{
"phishing": "true",
"risk": "3"
} Decisions: Pii ssn email 0% right
{
"data_kind": {
"none": 0.9,
"contact": 0.05,
"financial": 0.03,
"government_id": 0.02,
"health": 0.0
},
"sensitive": "false"
} Decisions: Review mixed 0% right
{
"sentiment": "negative",
"defect": "true",
"recommend": "false"
} Documents: Saas escalator (hard) 0% right
{
"year1_invoice": 64800,
"year2_invoice": 74352,
"year3_invoice": 70200,
"year3_discount_percent": 5,
"year1_price_per_seat_month": 45,
"year2_price_per_seat_month": 47.85,
"year3_price_per_seat_month": 47.85,
"addon_months_billed": 9,
"addon_invoice": 25920,
"total_contract_value": 179040,
"contract_end_date": "2029-02-28"
} Documents: Expense thread 67% right
{
"employee_id": "EMP-20417",
"destination_city": "LIS",
"trip_start": "2025-02-24",
"trip_end": "2025-02-27",
"approved_items": [
{
"date": "2025-02-24",
"category": "airfare",
"amount_usd": 1094.16
},
{
"date": "2025-02-24",
"category": "ground_transport",
"amount_usd": 36.0
},
{
"date": "2025-02-25",
"category": "meals",
"amount_usd": 65.0
},
{
"date": "2025-02-26",
"category": "lodging",
"amount_usd": 392.64
},
{
"date": "2025-02-27",
"category": "ground_transport",
"amount_usd": 41.5
}
],
"rejected_item_count": 1,
"per_diem_days": 3,
"per_diem_usd": 65.0,
"total_reimbursable_usd": 1286.29,
"approver_email": "priya.raman@corvane.com"
} Documents: Lease amendment 67% 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": 2227.00,
"late_fee_from_2025_06_01": 107.50,
"security_deposit": 2150,
"total_pet_deposits": 800,
"total_monthly_payment_july_2025": 2255.00,
"move_in_payment": 3350
} Documents: Ticket SLA 73% right
{
"ticket_id": "48213",
"account_id": "ACC-7731",
"open_issue": "inventory_sync",
"resolved_issues": ["billing_address"],
"affected_orders": ["SO-99815", "SO-99820", "SO-99827"],
"priority": "P2",
"sla_due_local": "2025-09-13T09:00:00",
"sla_due_utc": "2025-09-13T15:00:00Z",
"reissued_invoice": "INV-2025-0812"
} Documents: Sales footnotes 0% right
{
"q3_total_usd": 4902,
"q2_total_usd": 4585,
"q2_central_originally_reported_usd": 3047,
"q2_to_q3_change_pct": null,
"top_region_q3": "West",
"fastest_growing_region_q1_to_q3": "East",
"regions_declining_q2_to_q3": ["Central"],
"international_q3_organic_usd": 1951,
"west_excluding_mountain_q3_usd": 2893
} Size: 3.2B parameters. First tested OCT 11.
Comments
Sign in with GitHub to comment. Spam and abuse are hidden automatically.