- Qwen3 Coder 30B A3B Instruct is a free model from Alibaba's Qwen team that you can run on your own computer. In our tests it's not one we'd recommend right now: 51 out of 100, #30 of 56.
- It solved 17 of 30 coding jobs and scored 46 on reading documents. On our hardest tasks it scored 42.
- Runs on a 24 GB graphics card or a Mac with 32 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. Qwen3 Coder 30B A3B Instruct got 17 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. Qwen3 Coder 30B A3B Instruct scored 46; the best model scored 100.
| Test | Score | Public questions | Secret questions |
|---|---|---|---|
| Coding | 57 | 43 | 61 |
| Reading documents | 46 | 62 | 42 |
| Decisions | 81 | 79 | 82 |
On the 18 hardest tasks (included in the scores above) it scored 42. This number separates the top models.
This model did much better on our public questions than on the secret ones. Sometimes that's luck; it can also mean the model has seen the public questions before.
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 Qwen3 Coder 30B A3B Instruct 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 | No | – |
| Graphics card, 12 GB | No | – |
| Graphics card, 16 GB | No | – |
| Graphics card, 24 GB | Yes | Q5 |
| Graphics card, 32 GB | Yes | Q6 |
| Mac, 16 GB | No | – |
| Mac, 24 GB | No | – |
| Mac, 32 GB | Yes | Q4 |
| 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
The easiest way is Ollama. Install it, then run:
ollama run qwen3-coder:30b Want the strongest all-rounder instead? Qwen3.6 27B scored 97.
Its actual answers
What Qwen3 Coder 30B A3B Instruct wrote for our public questions, word for word.
Coding: INI parser (hard) Didn't work
```python
import re
from collections import defaultdict
def parse_ini(text: str) -> dict[str, dict[str, str]]:
lines = text.splitlines()
# Parse the INI file
sections = {}
current_section = None
default_section = {}
key_lines = []
for line_num, line in enumerate(lines, 1):
stripped_line = line.strip()
# Skip empty lines and comments
if not stripped_line or stripped_line[0] in ';#':
continue
# Check for section header
if stripped_line.startswith('[') and stripped_line.endswith(']'):
section_name = stripped_line[1:-1].strip()
if not section_name:
raise ValueError(f"Line {line_num}: Empty section name")
current_section = section_name
sections[section_name] = {}
continue
# If we encounter a key line before any section, raise error
if current_section is None:
raise ValueError(f"Line {line_num}: Key value before any section header")
# Parse key-value pairs
# Allow both = and : as separators, but only the first one counts
if '=' in stripped_line:
sep_pos = stripped_line.find('=')
key_part = stripped_line[:sep_pos]
value_part = stripped_line[sep_pos+1:]
elif ':' in stripped_line:
sep_pos = stripped_line.find(':')
key_part = stripped_line[:sep_pos]
value_part = stripped_line[sep_pos+1:]
else:
raise ValueError(f"Line {line_num}: Malformed key-value line")
key = key_part.strip().lower()
value = value_part.strip()
# Check for inline comment
comment_start = value.find(' ;')
if comment_start == -1:
comment_start = value.find(' #')
if comment_start != -1:
value = value[:comment_start]
# Handle quoted values
if value.startswith('"') and value.endswith('"') and len(value) >= 2:
value = value[1:-1] # Remove quotes
else:
value = value.strip()
# Check for continuation
if key_lines and not key_lines[-1][0] and line.startswith(' '):
# This is a continuation line
prev_key, prev_value = key_lines[-1]
key_lines[-1] = (prev_key, prev_value + '\n' + value.strip())
else:
key_lines.append((key, value))
sections[current_section][key] = value
# Apply DEFAULT section to other sections
default_keys = default_section.copy()
# Handle interpolation
def interpolate_value(value: str, section_name: str, seen: list = None) -> str:
if seen is None:
seen = []
# Do interpolation
def replace_match(match):
if match.group(0) == '$':
return '$'
# Extract the reference
ref = match.group(1)
if ':' in ref:
sec, k = ref.split(':', 1)
else:
sec = section_name
k = ref
k = k.lower()
# Check if this is a cycle
if (sec, k) in seen:
raise ValueError(f"Circular reference in section '{section_name}' for key '{k}'")
# Get the value
if sec == 'DEFAULT':
if k not in default_keys:
raise KeyError(f"Key '{k}' not found in DEFAULT section")
val = default_keys[k]
elif sec not in sections:
raise KeyError(f"Section '{sec}' not found for reference '${ref}'")
elif k not in sections[sec]:
raise KeyError(f"Key '{k}' not found in section '{sec}'")
else:
val = sections[sec][k]
# Recursive interpolation
seen.append((sec, k))
try:
return interpolate_value(val, section_name, seen)
finally:
seen.pop()
# Replace all occurrences
pattern = r'\$\{([^}]+)\}'
return re.sub(pattern, replace_match, value)
# Process default keys
processed_sections = {}
for sec_name, sec_dict in sections.items():
if sec_name == 'DEFAULT':
default_keys = sec_dict.copy()
else:
processed_sections[sec_name] = {}
# First process all keys
for key, value in sec_dict.items():
processed_sections[sec_name][key] = interpolate_value(value, sec_name)
# Apply default keys to sections
# For each section that doesn't have a key, inherit from DEFAULT if needed
final_sections = {}
for sec_name, sec_dict in processed_sections.items():
final_sections[sec_name] = {}
# Add all default values where missing in current section
for key, val in default_keys.items():
if key not in sec_dict:
sec_dict[key] = val
# Apply interpolation to any remaining default values
for key, val in sec_dict.items():
final_sections[sec_name][key] = interpolate_value(val, sec_name)
return final_sections
``` Coding: TTL LRU (hard) Worked
```python
import collections
import time
class TTLCache:
def __init__(self, capacity: int, ttl: float, clock):
self.capacity = capacity
self.ttl = ttl
self.clock = clock
self.cache = {}
self.access_order = collections.OrderedDict()
self.stats_ = {"hits": 0, "misses": 0, "evictions": 0, "expirations": 0}
def _cleanup_expired(self):
now = self.clock()
expired_keys = []
for key, (value, expiry) in self.cache.items():
if now >= expiry:
expired_keys.append(key)
for key in expired_keys:
del self.cache[key]
self.access_order.pop(key, None)
self.stats_["expirations"] += 1
def _evict_lru(self):
if not self.access_order:
return
lru_key = next(iter(self.access_order))
del self.cache[lru_key]
self.access_order.pop(lru_key)
self.stats_["evictions"] += 1
def put(self, key, value):
if self.capacity == 0:
return
now = self.clock()
expiry = now + self.ttl
# Clean up expired entries first
self._cleanup_expired()
# If key already exists, update it (refresh expiry and recency)
if key in self.cache:
self.cache[key] = (value, expiry)
self.access_order.move_to_end(key)
return
# If cache is full, evict LRU
if len(self.cache) >= self.capacity:
self._evict_lru()
# Add new entry
self.cache[key] = (value, expiry)
self.access_order[key] = None
def get(self, key, default=None):
if self.capacity == 0:
self.stats_["misses"] += 1
return default
now = self.clock()
# Check if key exists
if key not in self.cache:
self.stats_["misses"] += 1
return default
value, expiry = self.cache[key]
# Check if expired
if now >= expiry:
del self.cache[key]
self.access_order.pop(key, None)
self.stats_["expirations"] += 1
self.stats_["misses"] += 1
return default
# Hit: update recency
self.access_order.move_to_end(key)
self.stats_["hits"] += 1
return value
def __len__(self):
if self.capacity == 0:
return 0
now = self.clock()
count = 0
for _, (value, expiry) in self.cache.items():
if now < expiry:
count += 1
return count
def stats(self):
return self.stats_.copy()
``` Coding: Parse duration Didn't work
```python
import re
def parse_duration(s: str) -> int:
if not s or not s.strip():
raise ValueError("Empty or blank string")
# Remove whitespace and split by unit
s = s.strip()
parts = re.split(r'([hms])', s)
# Remove empty strings from the split
parts = [p for p in parts if p]
# Check if we have an even number of parts (number + unit)
if len(parts) % 2 != 0:
raise ValueError("Invalid format: numbers must be followed by units")
# Build a dictionary of units we've seen
seen_units = set()
total_seconds = 0
unit_order = ['h', 'm', 's']
unit_map = {'h': 3600, 'm': 60, 's': 1}
# Process pairs of [number, unit]
for i in range(0, len(parts), 2):
number_str = parts[i]
unit = parts[i+1].lower()
# Check if the number is valid (non-negative integer)
if not re.match(r'^\d+$', number_str):
raise ValueError("Invalid number format")
# Convert number to int
num = int(number_str)
# Check if unit is valid
if unit not in unit_map:
raise ValueError("Unknown unit")
# Check if unit is already used
if unit in seen_units:
raise ValueError("Repeated unit")
# Check if unit is in correct order
if unit_order.index(unit) < unit_order.index(unit_order[0]) or \
(unit_order.index(unit) > 0 and unit_order.index(unit) - unit_order.index(unit_order[unit_order.index(unit)-1]) > 1):
# This is a simplified check that units appear in order
# We'll use a more direct approach - check if the unit is in the right position relative to previously seen units
pass
# More precise ordering check
if seen_units:
last_unit = None
for u in unit_order:
if u in seen_units:
last_unit = u
if u == unit:
break
# If there's a unit that should come before this one and we've already seen it, that's wrong
# But manually track this using sequence
if last_unit:
last_unit_idx = unit_order.index(last_unit)
current_unit_idx = unit_order.index(unit)
if current_unit_idx <= last_unit_idx:
raise ValueError("Units out of order")
# Add to seen units
seen_units.add(unit)
# Add to total seconds
total_seconds += num * unit_map[unit]
return total_seconds
``` Coding: Summarize ranges Worked
```python
def summarize_ranges(nums: list[int]) -> str:
if not nums:
return ""
# Sort and remove duplicates
unique_sorted = sorted(set(nums))
if not unique_sorted:
return ""
result = []
start = unique_sorted[0]
end = unique_sorted[0]
for i in range(1, len(unique_sorted)):
if unique_sorted[i] == end + 1:
# Consecutive number, extend the current range
end = unique_sorted[i]
else:
# Non-consecutive number, finalize the previous range
if start == end:
result.append(str(start))
elif end - start >= 2:
result.append(f"{start}..{end}")
else:
result.append(str(start))
result.append(str(end))
# Start a new range
start = unique_sorted[i]
end = unique_sorted[i]
# Handle the last range
if start == end:
result.append(str(start))
elif end - start >= 2:
result.append(f"{start}..{end}")
else:
result.append(str(start))
result.append(str(end))
return ",".join(result)
``` Coding: Top customers Didn't work
```python
import re
def top_customers(rows: list[dict], n: int) -> list[tuple[str, float]]:
customer_totals = {}
customer_names = {}
for row in rows:
customer = row.get("customer", "").strip()
amount_str = row.get("amount", "")
# Skip empty customer names
if not customer:
continue
# Parse amount
# Remove non-numeric characters except decimal point and minus sign
clean_amount = re.sub(r'[^\d.-]', '', amount_str)
if not clean_amount or (clean_amount[0] == '-' and len(clean_amount) == 1):
continue
# Handle negative amounts properly by looking for minus sign not at start
if clean_amount.count('-') > 1:
# More than one minus sign, invalid format
continue
try:
amount = float(clean_amount)
except ValueError:
continue
# Normalize customer name (case-insensitive)
customer_lower = customer.lower()
# Store first occurrence of customer name (trimmed)
if customer_lower not in customer_names:
customer_names[customer_lower] = customer
# Add to total
if customer_lower in customer_totals:
customer_totals[customer_lower] += amount
else:
customer_totals[customer_lower] = amount
# Round totals to 2 decimal places
rounded_totals = [(name, round(total, 2)) for name, total in customer_totals.items()]
# Sort by total descending, then name ascending
sorted_totals = sorted(rounded_totals, key=lambda x: (-x[1], x[0].lower()))
# Return top n
return sorted_totals[:n]
``` Coding: Slugify Didn't work
```python
import unicodedata
import re
def slugify(title: str, max_len: int = 50) -> str:
# Step 1: Transliterate accented characters to ASCII and drop non-ASCII
normalized = unicodedata.normalize('NFKD', title)
ascii_title = normalized.encode('ascii', 'ignore').decode('ascii')
# Step 2: Lowercase
ascii_title = ascii_title.lower()
# Step 3: Replace runs of non-alphanumeric characters with "-"
slug = re.sub(r'[^a-z0-9]+', '-', ascii_title)
# Strip leading/trailing "-"
slug = slug.strip('-')
# Step 4: Truncate if longer than max_len
if len(slug) > max_len:
# If even the first word is too long, hard-cut it
if '-' not in slug or len(slug.split('-')[0]) > max_len:
slug = slug[:max_len]
else:
# Keep longest prefix of whole words within max_len
words = slug.split('-')
result = []
length = 0
for word in words:
if length + len(word) + (1 if result else 0) > max_len:
break
result.append(word)
length += len(word) + (1 if result else 0)
slug = '-'.join(result)
# Step 5: Ensure it doesn't end with "-"
slug = slug.rstrip('-')
return slug
``` Coding: Token bucket Worked
```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
self._last_refill = clock()
def _refill(self) -> None:
now = self.clock()
elapsed = now - self._last_refill
self._tokens += elapsed * self.refill_per_sec
self._tokens = min(self._tokens, self.capacity)
self._last_refill = now
def allow(self, cost: float = 1) -> bool:
self._refill()
if self._tokens >= cost:
self._tokens -= cost
return True
return False
@property
def tokens(self) -> float:
self._refill()
return self._tokens
``` Decisions: Refund window (hard) 50% right
{
"outcome": {
"full_refund": 0.0,
"partial_refund": 0.85,
"store_credit": 0.10,
"nothing": 0.05
},
"defective": {
"true": 0.1,
"false": 0.9
}
} Decisions: Incident matrix (hard) 0% right
{
"severity": {
"0": 0.0,
"1": 0.3,
"2": 0.6,
"3": 0.1
},
"page": {
"true": 0.7,
"false": 0.3
}
} Decisions: Tool followup (hard) 50% right
{
"tool": {
"calendar": 1.0
},
"confirm": {
"false": 1.0
}
} Decisions: Legit security alert (hard) 100% right
{
"phishing": {
"true": 0.05,
"false": 0.95
},
"action_needed": {
"true": 0.1,
"false": 0.9
}
} Decisions: Meeting slot (hard) 0% right
{
"slot": {
"A": 0.0,
"B": 0.0,
"C": 0.0,
"D": 1.0
},
"raj_last": {
"true": 0.3,
"false": 0.7
}
} Decisions: Review mixed (hard) 100% right
{
"hardware": {
"true": 0.85,
"false": 0.15
},
"support": {
"true": 0.9,
"false": 0.1
}
} Decisions: Support checkout down 100% right
{
"department": {
"billing": 0.1,
"technical": 0.8,
"account": 0.05,
"shipping": 0.03,
"sales": 0.02
},
"urgency": {
"0": 0.01,
"1": 0.05,
"2": 0.2,
"3": 0.74
},
"outage": {
"true": 0.95,
"false": 0.05
}
} Decisions: Refund wrong plan 100% right
{
"department": {
"billing": 0.95,
"technical": 0.02,
"account": 0.02,
"shipping": 0.005,
"sales": 0.005
},
"refund": {
"true": 0.9,
"false": 0.1
},
"tone": {
"frustrated": 0.1,
"calm": 0.9
}
} Decisions: Moderation doxxing 100% right
{
"policy": {
"none": 0.1,
"harassment": 0.8,
"hate": 0.05,
"spam": 0.03,
"self_harm": 0.02
},
"personal_info": {
"true": 0.95,
"false": 0.05
}
} Decisions: Route calendar 100% right
{
"tool": {
"calendar": 0.95,
"web_search": 0.03,
"calculator": 0.01,
"email": 0.005,
"none": 0.005
},
"confirm": {
"true": 0.9,
"false": 0.1
}
} Decisions: Doc invoice missing due 100% right
{
"doc_type": {
"invoice": 0.95,
"resume": 0.01,
"contract": 0.02,
"bank_statement": 0.01,
"other": 0.01
},
"missing_due_date": {
"true": 0.85,
"false": 0.15
}
} Decisions: Phishing paypal 100% right
{
"phishing": {
"true": 0.95,
"false": 0.05
},
"risk": {
"0": 0.01,
"1": 0.04,
"2": 0.25,
"3": 0.70
}
} Decisions: Pii ssn email 100% right
{
"data_kind": {
"none": 0,
"contact": 0,
"financial": 0,
"government_id": 1,
"health": 0
},
"sensitive": {
"true": 1,
"false": 0
}
} Decisions: Review mixed 100% right
{
"sentiment": {
"positive": 0.1,
"neutral": 0.1,
"negative": 0.8
},
"defect": {
"true": 0.9,
"false": 0.1
},
"recommend": {
"true": 0.1,
"false": 0.9
}
} Documents: Saas escalator (hard) 10% right
{
"year2_price_per_seat_month": 46.57,
"year3_price_per_seat_month": 46.57,
"year1_invoice": 64800.00,
"year2_invoice": 66470.40,
"addon_months_billed": 4,
"addon_invoice": 18628.00,
"year3_invoice": 66470.40,
"year3_discount_percent": 10.00,
"total_contract_value": 197748.80,
"contract_end_date": "2027-02-28"
} Documents: Expense thread 88% 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": 39.36
},
{
"date": "2025-02-25",
"category": "meals",
"amount_usd": 229.39
},
{
"date": "2025-02-26",
"category": "lodging",
"amount_usd": 432.00
},
{
"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": 1935.17,
"approver_email": "priya.raman@corvane.com"
} Documents: Lease amendment 83% 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": 2551,
"move_in_payment": 2550
} Documents: Ticket SLA 75% 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-12T16:05",
"sla_due_utc": "2025-09-12T21:05:00Z",
"reissued_invoice": "INV-2025-0812"
} Documents: Sales footnotes 56% right
{
"q3_total_usd": 16456000,
"q2_total_usd": 12454000,
"q2_central_originally_reported_usd": 3047000,
"q2_to_q3_change_pct": 8.6,
"top_region_q3": "East",
"fastest_growing_region_q1_to_q3": "Central",
"regions_declining_q2_to_q3": ["East"],
"international_q3_organic_usd": 1731000,
"west_excluding_mountain_q3_usd": 4201000
} Size: 31B parameters. First tested OCT 10.
Comments
Sign in with GitHub to comment. Spam and abuse are hidden automatically.