Review · updated OCT 11

Qwen3 Coder 30B A3B Instruct review: not one we'd recommend right now

It scored 51 out of 100, #30 of 56. It solved 17 of 30 coding jobs and scored 46 on reading documents. Runs on a 24 GB graphics card or a Mac with 32 GB.

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

TestScorePublic questionsSecret questions
Coding574361
Reading documents466242
Decisions817982

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 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-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.

Models that scored about the same

Comments

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