Review · updated OCT 11

Qwen3 30B A3B review: decent, with trade-offs

It scored 77 out of 100, #16 of 56. It solved 20 of 30 coding jobs and scored 87 on reading documents. Runs on a 24 GB graphics card or a Mac with 32 GB.

The short version
  • 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.

TestScorePublic questionsSecret questions
Coding677165
Reading documents879286
Decisions9610094

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 computerRuns it?Version to download
Graphics card, 8 GBNo–
Graphics card, 12 GBNo–
Graphics card, 16 GBNo–
Graphics card, 24 GBYesQ5
Graphics card, 32 GBYesQ6
Mac, 16 GBNo–
Mac, 24 GBNo–
Mac, 32 GBYesQ4
Mac, 48 GBYesQ8
Mac, 64 GBYesQ8
Mac, 96 GBYesQ8
Mac, 128 GBYesQ8

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.

Models that scored about the same

Comments

Sign in with GitHub to comment. Spam and abuse are hidden automatically.