- Qwen3 30B A3B is a free model from Alibaba's Qwen team that you can run on your own computer. In our tests it's decent, with trade-offs: 77 out of 100, #16 of 56.
- It solved 20 of 30 coding jobs and scored 87 on reading documents. On our hardest tasks it scored 50.
- 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 30B A3B got 20 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 30B A3B scored 87; the best model scored 100.
| Test | Score | Public questions | Secret questions |
|---|---|---|---|
| Coding | 67 | 71 | 65 |
| Reading documents | 87 | 92 | 86 |
| Decisions | 96 | 100 | 94 |
On the 18 hardest tasks (included in the scores above) it scored 50. 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 Qwen3 30B A3B 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:30b Want the strongest all-rounder instead? Qwen3.6 27B scored 97.
Its actual answers
What Qwen3 30B A3B wrote for our public questions, word for word.
Coding: INI parser (hard) Didn't work
```python
import re
def parse_ini(text: str) -> dict[str, dict[str, str]]:
sections = {}
default_dict = {}
current_section = None
current_key = None
current_value = None
previous_line_was_key = False
lines = text.splitlines()
def interpolate(text, current_section, default_dict, sections, path=None):
if path is None:
path = []
match = re.search(r'\$\{([^}]*)\}', text)
if not match:
return text
key_part = match.group(1)
if ':' in key_part:
ref_section, ref_key = key_part.split(':', 1)
else:
ref_section = current_section
ref_key = key_part
if (ref_section, ref_key) in path:
raise ValueError(f"Cycle detected in interpolation: {ref_key} in {ref_section}")
new_path = path + [(ref_section, ref_key)]
if ref_section == 'DEFAULT':
if ref_key in default_dict:
replacement = default_dict[ref_key]
else:
raise KeyError(f"Key {ref_key} not found in DEFAULT section")
else:
if ref_section not in sections:
raise KeyError(f"Section {ref_section} not found")
if ref_key in sections[ref_section]:
replacement = sections[ref_section][ref_key]
else:
if ref_key in default_dict:
replacement = default_dict[ref_key]
else:
raise KeyError(f"Key {ref_key} not found in section {ref_section} or DEFAULT")
new_text = text[:match.start()] + replacement + text[match.end():]
return interpolate(new_text, current_section, default_dict, sections, new_path)
for line in lines:
stripped_line = line.strip()
if not stripped_line or stripped_line.startswith(';') or stripped_line.startswith('#'):
previous_line_was_key = False
continue
section_match = re.match(r'^\s*\[([^\]]+)\]\s*$', line)
if section_match:
section_name = section_match.group(1).strip()
if section_name == 'DEFAULT':
current_section = 'DEFAULT'
current_key = None
current_value = None
else:
current_section = section_name
if current_section not in sections:
sections[current_section] = {}
current_key = None
current_value = None
previous_line_was_key = False
continue
if line.lstrip() != line:
if current_key is not None:
current_value += '\n' + line.strip()
previous_line_was_key = False
continue
else:
raise ValueError("Continuation line without previous key")
if current_key is not None:
if current_section == 'DEFAULT':
default_dict[current_key] = current_value
else:
sections[current_section][current_key] = current_value
current_key = None
current_value = None
main_part = line
comment_pos = -1
for i in range(len(main_part)):
if i + 1 < len(main_part) and main_part[i] == ' ' and main_part[i+1] in (';', '#'):
comment_pos = i
break
if comment_pos != -1:
main_part = main_part[:comment_pos]
split_pos = None
for i, c in enumerate(main_part):
if c in ('=', ':'):
split_pos = i
break
if split_pos is None:
raise ValueError("Invalid key line")
key_str = main_part[:split_pos].strip().lower()
value_str = main_part[split_pos+1:].strip()
if value_str.startswith('"') and value_str.endswith('"'):
value = value_str[1:-1]
else:
value = value_str
try:
interpolated_value = interpolate(value, current_section, default_dict, sections)
except KeyError as e:
raise e
except ValueError as e:
raise e
current_key = key_str
current_value = interpolated_value
previous_line_was_key = True
if current_key is not None:
if current_section == 'DEFAULT':
default_dict[current_key] = current_value
else:
sections[current_section][current_key] = current_value
result = {}
for section in sections:
merged = {}
merged.update(default_dict)
merged.update(sections[section])
result[section] = merged
return result
``` Coding: TTL LRU (hard) Didn't work
```python
from collections import OrderedDict
import time
class TTLCache:
def __init__(self, capacity: int, ttl: float, clock):
self.capacity = capacity
self.ttl = ttl
self.clock = clock
self._cache = OrderedDict()
self.stats = {
'hits': 0,
'misses': 0,
'evictions': 0,
'expirations': 0,
}
def put(self, key, value):
if self.capacity == 0:
return
current_time = self.clock()
if key in self._cache:
self._cache[key] = (value, current_time + self.ttl)
self._cache.move_to_end(key, last=True)
else:
expired_keys = [k for k in self._cache if self._cache[k][1] <= current_time]
for k in expired_keys:
del self._cache[k]
if len(self._cache) >= self.capacity:
evicted_key, _ = self._cache.popitem(last=False)
self.stats['evictions'] += 1
self._cache[key] = (value, current_time + self.ttl)
self._cache.move_to_end(key, last=True)
def get(self, key, default=None):
current_time = self.clock()
if key not in self._cache:
self.stats['misses'] += 1
return default
expiry = self._cache[key][1]
if expiry <= current_time:
del self._cache[key]
self.stats['misses'] += 1
self.stats['expirations'] += 1
return default
else:
self._cache.move_to_end(key, last=True)
self.stats['hits'] += 1
return self._cache[key][0]
def __len__(self):
current_time = self.clock()
count = 0
for key in self._cache:
if self._cache[key][1] > current_time:
count += 1
return count
def stats(self):
return self.stats.copy()
``` Coding: Parse duration Worked
```python
import re
def parse_duration(s: str) -> int:
s_clean = s.replace(' ', '')
if not s_clean:
raise ValueError("Empty string")
parts = re.findall(r'(\d+)([hmsHMS])', s_clean)
parsed_str = ''.join([num + unit for num, unit in parts])
if parsed_str != s_clean:
raise ValueError("Invalid format")
units = []
for num_str, unit in parts:
unit_lower = unit.lower()
if unit_lower not in {'h', 'm', 's'}:
raise ValueError("Unknown unit")
if unit_lower in units:
raise ValueError("Repeated unit")
units.append(unit_lower)
for i in range(1, len(units)):
if units[i] < units[i-1]:
raise ValueError("Units out of order")
total = 0
for num_str, unit in parts:
num = int(num_str)
unit_lower = unit.lower()
if unit_lower == 'h':
total += num * 3600
elif unit_lower == 'm':
total += num * 60
elif unit_lower == 's':
total += num
return total
``` Coding: Summarize ranges Worked
```python
def summarize_ranges(nums: list[int]) -> str:
if not nums:
return ""
sorted_nums = sorted(nums)
sorted_unique = []
prev = None
for num in sorted_nums:
if num != prev:
sorted_unique.append(num)
prev = num
groups = []
current_start = sorted_unique[0]
current_end = sorted_unique[0]
for i in range(1, len(sorted_unique)):
num = sorted_unique[i]
if num == current_end + 1:
current_end = num
else:
groups.append((current_start, current_end))
current_start = num
current_end = num
groups.append((current_start, current_end))
parts = []
for start, end in groups:
if end - start >= 2:
parts.append(f"{start}..{end}")
else:
for num in range(start, end + 1):
parts.append(str(num))
return ','.join(parts)
``` Coding: Top customers Worked
```python
def top_customers(rows: list[dict], n: int) -> list[tuple[str, float]]:
customer_data = {}
for row in rows:
customer = row.get('customer', '').strip()
if not customer:
continue
amount_str = row.get('amount', '')
try:
cleaned = amount_str.replace('$', '').replace(',', '')
amount = float(cleaned)
except ValueError:
continue
lower_customer = customer.lower()
if lower_customer not in customer_data:
customer_data[lower_customer] = (customer, amount)
else:
existing_customer, total = customer_data[lower_customer]
customer_data[lower_customer] = (existing_customer, total + amount)
result = []
for key in customer_data:
original_name, total = customer_data[key]
rounded_total = round(total, 2)
result.append((original_name, rounded_total))
result.sort(key=lambda x: (-x[1], x[0]))
return result[:n]
``` Coding: Slugify Worked
```python
import unicodedata
import re
def slugify(title: str, max_len: int = 50) -> str:
normalized = unicodedata.normalize('NFKD', title)
ascii_chars = [c for c in normalized if ord(c) <= 127]
ascii_str = ''.join(ascii_chars)
lower_str = ascii_str.lower()
step3 = re.sub(r'[^a-z0-9]+', '-', lower_str).strip('-')
if len(step3) <= max_len:
return step3
words = step3.split('-')
prefix = []
current_length = 0
for word in words:
if not prefix:
possible_length = len(word)
else:
possible_length = current_length + len(word) + 1
if possible_length > max_len:
break
else:
prefix.append(word)
current_length = possible_length
if not prefix:
if words:
slug = words[0][:max_len]
else:
slug = ''
else:
slug = '-'.join(prefix)
return slug
``` 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.current_tokens = capacity
self.last_time = clock()
def _refill(self):
now = self.clock()
elapsed = now - self.last_time
self.current_tokens += self.refill_per_sec * elapsed
if self.current_tokens > self.capacity:
self.current_tokens = self.capacity
self.last_time = now
def allow(self, cost: float = 1) -> bool:
self._refill()
if self.current_tokens >= cost:
self.current_tokens -= cost
return True
else:
return False
@property
def tokens(self) -> float:
self._refill()
return self.current_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) 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: Legit security alert (hard) 100% right
{"phishing": {"true": 0.1, "false": 0.9}, "action_needed": {"true": 0.1, "false": 0.9}} 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": 1.0,
"false": 0.0
},
"support": {
"true": 1.0,
"false": 0.0
}
} Decisions: Support checkout down 100% right
{
"department": {
"billing": 0.3,
"technical": 0.7,
"account": 0.0,
"shipping": 0.0,
"sales": 0.0
},
"urgency": {
"0": 0.0,
"1": 0.0,
"2": 0.0,
"3": 1.0
},
"outage": {
"true": 1.0,
"false": 0.0
}
} 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": {
"frustrated": 0.0,
"calm": 1.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": 0.95, "false": 0.05}, "risk": {"0": 0, "1": 0, "2": 0.05, "3": 0.95}} Decisions: Pii ssn email 100% right
{
"data_kind": {
"none": 0.0,
"contact": 0.0,
"financial": 0.2,
"government_id": 0.8,
"health": 0.0
},
"sensitive": {
"true": 1.0,
"false": 0.0
}
} Decisions: Review mixed 100% right
{
"sentiment": {
"positive": 0.1,
"neutral": 0.1,
"negative": 0.8
},
"defect": {
"true": 1.0,
"false": 0.0
},
"recommend": {
"true": 0.0,
"false": 1.0
}
} Documents: Saas escalator (hard) 70% right
{
"year2_price_per_seat_month": 47.25,
"year3_price_per_seat_month": 47.25,
"year1_invoice": 64800.00,
"year2_invoice": 68040.00,
"addon_months_billed": 6,
"addon_invoice": 38556.00,
"year3_invoice": 134946.00,
"year3_discount_percent": 15.00,
"total_contract_value": 306342.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.6}, {"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, "total_reimbursable_usd": 2159.25, "approver_email": "priya.raman@corvane.com"} Documents: Lease amendment 100% 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": 4700
} 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 100% right
{
"q3_total_usd": 15346000,
"q2_total_usd": 14464000,
"q2_central_originally_reported_usd": 3047000,
"q2_to_q3_change_pct": 6.1,
"top_region_q3": "East",
"fastest_growing_region_q1_to_q3": "International",
"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.