- Qwen3 VL 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: 46 out of 100, #32 of 56.
- It solved 13 of 30 coding jobs and scored 49 on reading documents. On our hardest tasks it scored 33.
- 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 VL 30B A3B Instruct got 13 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 VL 30B A3B Instruct scored 49; the best model scored 100.
| Test | Score | Public questions | Secret questions |
|---|---|---|---|
| Coding | 43 | 43 | 44 |
| Reading documents | 49 | 66 | 44 |
| Decisions | 75 | 71 | 76 |
On the 18 hardest tasks (included in the scores above) it scored 33. 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 VL 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
Search for "Qwen3 VL 30B A3B Instruct GGUF" in LM Studio or on Hugging Face and pick the version that fits your computer. New to this? Start with our Ollama guide.
Want the strongest all-rounder instead? Qwen3.6 27B scored 97.
Its actual answers
What Qwen3 VL 30B A3B Instruct wrote for our public questions, word for word.
Coding: INI parser (hard) Didn't work
```python
import re
from typing import Dict, List, Tuple
def parse_ini(text: str) -> Dict[str, Dict[str, str]]:
lines = text.splitlines()
result: Dict[str, Dict[str, str]] = {}
current_section: str = None
default_section: Dict[str, str] = {}
i = 0
# Helper to strip and check if line is empty or comment
def is_comment_or_empty(line: str) -> bool:
stripped = line.strip()
return len(stripped) == 0 or stripped[0] in (';', '#')
# Helper to parse a key-value line
def parse_key_value(line: str, section: str) -> Tuple[str, str]:
# Strip leading/trailing whitespace
line = line.strip()
if not line:
raise ValueError("Malformed line: empty after stripping")
# Check for inline comment
if ' ;' in line or ' #' in line:
# Find first occurrence of ' ;' or ' #' after any leading whitespace
# But only if it's not inside double quotes
in_quotes = False
comment_start = -1
for j, char in enumerate(line):
if char == '"':
in_quotes = not in_quotes
elif not in_quotes and (char == ';' or char == '#'):
if j > 0 and line[j-1] == ' ':
comment_start = j
break
if comment_start != -1:
line = line[:comment_start].rstrip()
# Check if line is empty after removing comment
if not line:
raise ValueError("Malformed line: empty after removing comment")
# Split on first = or :, whichever comes first
if '=' in line and ':' in line:
split_pos = min(line.find('='), line.find(':'))
elif '=' in line:
split_pos = line.find('=')
elif ':' in line:
split_pos = line.find(':')
else:
raise ValueError(f"Malformed line: no '=' or ':' found in '{line}'")
key_part = line[:split_pos].strip()
value_part = line[split_pos + 1:].strip()
# Check if value is quoted
if value_part.startswith('"') and value_part.endswith('"'):
value = value_part[1:-1] # Remove quotes, keep everything inside
else:
value = value_part
# Key is stripped and lowercased
key = key_part.lower()
# Validate key
if not key:
raise ValueError(f"Malformed line: empty key in '{line}'")
return key, value
# Helper to interpolate values
def interpolate_value(value: str, section: str, all_sections: Dict[str, Dict[str, str]]) -> str:
# Find all ${...} patterns
pattern = r'\$\{([^}]+)\}'
matches = re.findall(pattern, value)
if not matches:
return value
# Process each match
for match in matches:
if ':' in match:
# ${section:key}
section_name, key_name = match.split(':', 1)
if section_name not in all_sections:
raise KeyError(f"Section '{section_name}' not found")
if key_name not in all_sections[section_name]:
raise KeyError(f"Key '{key_name}' not found in section '{section_name}'")
replacement = all_sections[section_name][key_name]
else:
# ${key}
# Check current section first, then DEFAULT
if section not in all_sections:
raise KeyError(f"Section '{section}' not found")
if match in all_sections[section]:
replacement = all_sections[section][match]
elif match in default_section:
replacement = default_section[match]
else:
raise KeyError(f"Key '{match}' not found in section '{section}' or DEFAULT")
# Replace with interpolated value
value = value.replace(f'${{{match}}}', replacement)
# Check for cycles
if '${' in value:
# Recursive interpolation
return interpolate_value(value, section, all_sections)
return value
# Helper to handle continuation lines
def handle_continuation(lines: List[str], i: int, section: str, key: str, current_value: str) -> Tuple[int, str]:
while i < len(lines):
line = lines[i]
i += 1
stripped = line.strip()
if not stripped:
continue
if stripped[0] in (';', '#'):
continue
if ' ;' in stripped or ' #' in stripped:
# Remove inline comment
in_quotes = False
comment_start = -1
for j, char in enumerate(stripped):
if char == '"':
in_quotes = not in_quotes
elif not in_quotes and (char == ';' or char == '#'):
if j > 0 and stripped[j-1] == ' ':
comment_start = j
break
if comment_start != -1:
stripped = stripped[:comment_start].rstrip()
if not stripped:
continue
# Check if this line starts with whitespace (continuation)
if line.lstrip() != line:
# This is a continuation line
if stripped.startswith('"') and stripped.endswith('"'):
current_value += '\n' + stripped[1:-1]
else:
current_value += '\n' + stripped
else:
# Not a continuation line, break
break
return i, current_value
# Main parsing loop
while i < len(lines):
line = lines[i]
i += 1
# Skip empty lines or comments
if is_comment_or_empty(line):
continue
# Check for section header
if line.strip().startswith('[') and line.strip().endswith(']'):
section_name = line.strip()[1:-1].strip()
if section_name == 'DEFAULT':
current_section = None
# We'll handle DEFAULT separately
continue
else:
current_section = section_name
if current_section not in result:
result[current_section] = {}
continue
# If we're not in a section, and not DEFAULT, this is invalid
if current_section is None and not line.strip().startswith('['):
raise ValueError("Key line before any section header")
# Parse key-value
try:
key, value = parse_key_value(line, current_section)
except ValueError as e:
raise ValueError(f"Malformed line: {e}")
# Handle continuation lines
if i < len(lines):
next_line = lines[i]
if next_line.strip() and next_line.lstrip() != next_line:
# Next line starts with whitespace -> continuation
i, value = handle_continuation(lines, i, current_section, key, value)
# Interpolate value
try:
value = interpolate_value(value, current_section, result)
except KeyError as e:
raise KeyError(str(e))
except ValueError as e:
raise ValueError(str(e))
# Store in current section or DEFAULT
if current_section is None:
# This is a key in DEFAULT section
default_section[key] = value
else:
# Store in current section, overwrite if key exists
result[current_section][key] = value
# Apply DEFAULT inheritance to all sections
for section_name, section in result.items():
for key, value in default_section.items():
if key not in section:
section[key] = value
return result
``` Coding: TTL LRU (hard) Worked
```python
from collections import OrderedDict
from typing import Any, Optional, Dict
class TTLCache:
def __init__(self, capacity: int, ttl: float, clock):
self.capacity = capacity
self.ttl = ttl
self.clock = clock
self.data: OrderedDict[str, tuple[Any, float]] = OrderedDict() # key -> (value, expiry_time)
self.hits = 0
self.misses = 0
self.evictions = 0
self.expirations = 0
def _expire_old_entries(self):
"""Remove all expired entries from the cache."""
now = self.clock()
keys_to_remove = []
for key, (value, expiry) in self.data.items():
if now >= expiry:
keys_to_remove.append(key)
for key in keys_to_remove:
del self.data[key]
self.expirations += 1
def _evict_lru(self):
"""Evict the least recently used entry if cache is at capacity."""
if len(self.data) >= self.capacity and self.capacity > 0:
self.data.popitem(last=False)
self.evictions += 1
def put(self, key: str, value: Any):
"""Insert or replace the value for the key."""
if self.capacity == 0:
return
now = self.clock()
expiry = now + self.ttl
# Remove expired entries first
self._expire_old_entries()
# If key exists, update it (refresh expiry and recency)
if key in self.data:
self.data[key] = (value, expiry)
self.data.move_to_end(key) # mark as most recently used
else:
# If adding new key would exceed capacity, evict LRU if needed
if len(self.data) >= self.capacity:
self._evict_lru()
# Add new key
self.data[key] = (value, expiry)
def get(self, key: str, default: Optional[Any] = None) -> Any:
"""Return value for key if present and not expired, else default."""
if self.capacity == 0:
self.misses += 1
return default
now = self.clock()
# Check if key exists and is not expired
if key in self.data:
value, expiry = self.data[key]
if now < expiry:
# Hit: update recency and return value
self.data.move_to_end(key)
self.hits += 1
return value
else:
# Expired: remove it and count as expiration and miss
del self.data[key]
self.expirations += 1
self.misses += 1
return default
else:
# Key not found
self.misses += 1
return default
def __len__(self) -> int:
"""Return number of non-expired entries."""
now = self.clock()
return sum(1 for _, (value, expiry) in self.data.items() if now < expiry)
def stats(self) -> Dict[str, int]:
"""Return statistics as a dictionary."""
return {
"hits": self.hits,
"misses": self.misses,
"evictions": self.evictions,
"expirations": self.expirations
}
``` Coding: Parse duration Didn't work
```python
import re
def parse_duration(s: str) -> int:
s = s.strip()
if not s:
raise ValueError("Empty or blank string")
# Split by whitespace and filter out empty strings
parts = [p for p in s.split() if p]
# Define allowed units (case-insensitive)
units = {'h': 3600, 'm': 60, 's': 1}
# Track which units we've already seen
used_units = set()
total_seconds = 0
for part in parts:
# Check if part ends with a valid unit
if not part or not part[-1].isalpha():
raise ValueError("Number without a unit")
unit_char = part[-1].lower()
if unit_char not in units:
raise ValueError(f"Unknown unit: {unit_char}")
# Extract the numeric part
num_str = part[:-1]
if not num_str.isdigit():
raise ValueError("Decimal numbers not allowed")
num = int(num_str)
if num < 0:
raise ValueError("Negative numbers not allowed")
# Check if unit is already used
if unit_char in used_units:
raise ValueError(f"Repeated unit: {unit_char}")
# Check order: h, then m, then s
if used_units and not (used_units == {'h'} and unit_char == 'm' or
used_units == {'h', 'm'} and unit_char == 's'):
raise ValueError("Units must appear in order: h, m, s")
# Add to total
total_seconds += num * units[unit_char]
used_units.add(unit_char)
# Check if we have all required units in correct order
# We don't require all units to be present, but if present, they must be in order
# The order check above ensures this
return total_seconds
``` Coding: Summarize ranges Worked
```python
def summarize_ranges(nums: list[int]) -> str:
if not nums:
return ""
# Remove duplicates and sort
sorted_unique = sorted(set(nums))
# Group consecutive numbers
ranges = []
start = sorted_unique[0]
current = start
for i in range(1, len(sorted_unique)):
if sorted_unique[i] == current + 1:
current = sorted_unique[i]
else:
# End of current run
if current - start >= 2: # 3 or more consecutive numbers
ranges.append(f"{start}..{current}")
else:
# 1 or 2 numbers
for j in range(start, current + 1):
ranges.append(str(j))
start = sorted_unique[i]
current = start
# Handle the last run
if current - start >= 2:
ranges.append(f"{start}..{current}")
else:
for j in range(start, current + 1):
ranges.append(str(j))
return ",".join(ranges)
``` Coding: Top customers Didn't work
```python
import re
from typing import List, Dict, Tuple
def top_customers(rows: List[Dict], n: int) -> List[Tuple[str, float]]:
customer_totals = {}
for row in rows:
customer = row.get("customer")
amount_str = row.get("amount")
# Skip if customer name is empty or cannot be parsed
if not customer or not isinstance(customer, str):
continue
customer = customer.strip()
if not customer:
continue
# Skip if amount is not a string or cannot be parsed
if not isinstance(amount_str, str):
continue
# Clean amount string
amount_str = amount_str.strip()
if not amount_str:
continue
# Remove currency symbols and commas, handle negative amounts
clean_amount = re.sub(r'[^\d.-]', '', amount_str)
if not clean_amount or not re.match(r'^-?\d+(\.\d+)?$', clean_amount):
continue
try:
amount = float(clean_amount)
except ValueError:
continue
# Use the first valid customer name (trimmed) as the key
if customer not in customer_totals:
customer_totals[customer] = 0.0
customer_totals[customer] += amount
# Sort by total descending, then by name ascending
result = sorted(customer_totals.items(), key=lambda x: (-x[1], x[0]))
# Return top n customers
return result[:n]
``` Coding: Slugify Didn't work
```python
import re
from unicodedata import normalize
def slugify(title: str, max_len: int = 50) -> str:
# Normalize to NFD to separate accents, then remove non-ASCII characters
normalized = normalize('NFD', title)
ascii_only = ''.join(c for c in normalized if c.isascii() and (c.isalpha() or c.isdigit() or c.isspace()))
# Lowercase
lowercase = ascii_only.lower()
# Replace non-alphanumeric (except spaces) with hyphens, then collapse multiple hyphens
slug = re.sub(r'[^a-z0-9\s]+', '-', lowercase)
slug = re.sub(r'\s+', ' ', slug) # Replace multiple spaces with single space
slug = re.sub(r'[^a-z0-9]+', '-', slug) # Replace non-alphanumeric (except spaces) with hyphens
slug = slug.strip('-')
# If slug is longer than max_len, shorten without cutting words
if len(slug) > max_len:
# Split into words (separated by hyphens)
words = slug.split('-')
result_words = []
current_length = 0
for word in words:
if current_length + len(word) + (1 if result_words else 0) <= max_len:
result_words.append(word)
current_length += len(word) + (1 if result_words else 0)
else:
break
slug = '-'.join(result_words)
# If first word is too long, hard-cut it
if len(slug) > max_len:
slug = slug[:max_len]
# Ensure it doesn't end with a hyphen
slug = slug.rstrip('-')
return slug
``` Coding: Token bucket Worked
```python
import time
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._tokens = capacity
self._last_refill_time = clock()
def _refill(self):
now = self.clock()
elapsed = now - self._last_refill_time
if elapsed > 0:
refill_amount = elapsed * self.refill_per_sec
self._tokens = min(self.capacity, self._tokens + refill_amount)
self._last_refill_time = 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) 0% right
{"outcome": {"full_refund": 0.0, "partial_refund": 0.0, "store_credit": 0.0, "nothing": 0.0}, "defective": "false"} Decisions: Incident matrix (hard) 0% right
{"severity": {"0": 0.0, "1": 0.0, "2": 0.7, "3": 0.3}, "page": {"true": 0.9, "false": 0.1}} Decisions: Tool followup (hard) 0% right
{"tool": "calendar", "confirm": "true"} 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": 0.0}, "raj_last": "false"} 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": {
"technical": 0.95,
"billing": 0.03,
"account": 0.01,
"shipping": 0.01,
"sales": 0.00
},
"urgency": {
"3": 0.98,
"2": 0.02,
"1": 0.00,
"0": 0.00
},
"outage": {
"true": 0.99,
"false": 0.01
}
} Decisions: Refund wrong plan 100% right
{
"department": {
"billing": 0.95,
"technical": 0.02,
"account": 0.02,
"shipping": 0.01,
"sales": 0.0
},
"refund": {
"true": 0.98,
"false": 0.02
},
"tone": {
"frustrated": 0.1,
"calm": 0.9
}
} Decisions: Moderation doxxing 100% right
{
"policy": {
"none": 0.0,
"harassment": 0.95,
"hate": 0.03,
"spam": 0.01,
"self_harm": 0.01
},
"personal_info": {
"true": 0.98,
"false": 0.02
}
} Decisions: Route calendar 100% right
{
"tool": {
"calendar": 0.95,
"email": 0.03,
"none": 0.01,
"web_search": 0.01,
"calculator": 0.0
},
"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.98,
"false": 0.02
}
} Decisions: Phishing paypal 100% right
{
"phishing": {
"true": 0.98,
"false": 0.02
},
"risk": {
"0": 0.01,
"1": 0.01,
"2": 0.02,
"3": 0.96
}
} Decisions: Pii ssn email 100% right
{
"data_kind": {
"none": 0.0,
"contact": 0.1,
"financial": 0.0,
"government_id": 0.9,
"health": 0.0
},
"sensitive": {
"true": 0.95,
"false": 0.05
}
} Decisions: Review mixed 100% right
{
"sentiment": {
"positive": 0.05,
"neutral": 0.1,
"negative": 0.85
},
"defect": {
"true": 0.95,
"false": 0.05
},
"recommend": {
"true": 0.05,
"false": 0.95
}
} Documents: Saas escalator (hard) 30% right
{
"year2_price_per_seat_month": 47.37,
"year3_price_per_seat_month": 49.82,
"year1_invoice": 64800.0,
"year2_invoice": 66820.8,
"addon_months_billed": 6,
"addon_invoice": 39856.0,
"year3_invoice": 127577.6,
"year3_discount_percent": 15,
"total_contract_value": 299254.4,
"contract_end_date": "2027-02-28"
} Documents: Expense thread 83% 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": 38.88}, {"date": "2025-02-25", "category": "meals", "amount_usd": 229.40}, {"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": 1, "per_diem_usd": 65.0, "total_reimbursable_usd": 2489.26, "approver_email": "priya.raman@corvane.com"} Documents: Lease amendment 92% right
{"tenants": ["Marcus Lin", "Sofia Lin"], "landlord": "Ridgeline Property Group LLC", "zip": "97205", "lease_end": "2025-11-30", "original_monthly_rent": 2150.0, "monthly_rent_from_2025_06_01": 2236.0, "late_fee_from_2025_06_01": 111.8, "security_deposit": 2150.0, "total_pet_deposits": 800.0, "total_monthly_payment_july_2025": 2306.0, "move_in_payment": 5700.0} Documents: Ticket SLA 82% right
{
"ticket_id": "48213",
"account_id": "ACC-7731",
"open_issue": "inventory_sync",
"resolved_issues": ["billing_address"],
"affected_orders": ["SO-99812", "SO-99820", "SO-99827"],
"priority": "P2",
"sla_due_local": "2025-09-14T15:30",
"sla_due_utc": "2025-09-14T20:30:00Z",
"reissued_invoice": "INV-2025-0812"
} Documents: Sales footnotes 44% right
{
"q3_total_usd": 15346,
"q2_total_usd": 14464,
"q2_central_originally_reported_usd": 3047,
"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": 1731,
"west_excluding_mountain_q3_usd": 4201
} Size: 31B parameters. First tested OCT 10.
Comments
Sign in with GitHub to comment. Spam and abuse are hidden automatically.