- Qwen3.5-27B is a free model from Alibaba's Qwen team that you can run on your own computer. In our tests it's a solid all-rounder: 88 out of 100, #8 of 56.
- It solved 23 of 30 coding jobs and scored 100 on reading documents. On our hardest tasks it scored 78.
- 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.5-27B got 23 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.5-27B scored 100, the best result we've seen.
| Test | Score | Public questions | Secret questions |
|---|---|---|---|
| Coding | 77 | 86 | 74 |
| Reading documents | 100 | 98 | 100 |
| Decisions | 100 | 100 | 100 |
On the 18 hardest tasks (included in the scores above) it scored 78. 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.5-27B 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.5-27B 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.5-27B wrote for our public questions, word for word.
Coding: INI parser (hard) Didn't work
```python
import re
from typing import Optional
def parse_ini(text: str) -> dict[str, dict[str, str]]:
lines = text.split('\n')
sections: dict[str, dict[str, str]] = {}
current_section: Optional[str] = None
last_key: Optional[str] = None
i = 0
while i < len(lines):
line = lines[i]
# Check for continuation line
if line and line[0] in (' ', '\t') and last_key is not None and current_section is not None:
stripped = line.strip()
if stripped:
sections[current_section][last_key] += '\n' + stripped
i += 1
continue
stripped_line = line.strip()
# Skip empty lines
if not stripped_line:
i += 1
continue
# Skip comment lines
if stripped_line[0] in (';', '#'):
i += 1
continue
# Check for section header
if stripped_line.startswith('[') and ']' in stripped_line:
bracket_end = stripped_line.index(']')
section_name = stripped_line[1:bracket_end].strip()
current_section = section_name
if current_section not in sections:
sections[current_section] = {}
last_key = None
i += 1
continue
# Check for key-value pair
sep_pos = -1
for j, char in enumerate(stripped_line):
if char in ('=', ':'):
sep_pos = j
break
if sep_pos == -1:
raise ValueError(f"Malformed line: {line}")
if current_section is None:
raise ValueError("Key before any section header")
key = stripped_line[:sep_pos].strip().lower()
value_part = stripped_line[sep_pos + 1:]
value = _process_value(value_part)
sections[current_section][key] = value
last_key = key
i += 1
return _interpolate_sections(sections)
def _process_value(value_part: str) -> str:
value_part = value_part.lstrip()
if not value_part:
return ''
if value_part.startswith('"'):
i = 1
while i < len(value_part):
if value_part[i] == '"':
return value_part[1:i]
elif value_part[i] == '\\' and i + 1 < len(value_part):
i += 2
else:
i += 1
return value_part[1:]
result = []
i = 0
while i < len(value_part):
if value_part[i] in (' ', '\t') and i + 1 < len(value_part):
if value_part[i + 1] in (';', '#'):
break
result.append(value_part[i])
i += 1
return ''.join(result).strip()
def _interpolate_sections(sections: dict[str, dict[str, str]]) -> dict[str, dict[str, str]]:
result: dict[str, dict[str, str]] = {}
default_keys = sections.get('DEFAULT', {})
for section_name, section_data in sections.items():
if section_name == 'DEFAULT':
continue
merged = {**default_keys, **section_data}
interpolated: dict[str, str] = {}
for key, value in merged.items():
interpolated[key] = _interpolate_value(value, section_name, merged, set(), sections)
result[section_name] = interpolated
return result
def _interpolate_value(
value: str,
section_name: str,
all_values: dict[str, str],
visiting: set[tuple[str, str]],
sections: dict[str, dict[str, str]]
) -> str:
result = []
i = 0
while i < len(value):
if value[i:i+2] == '${':
end = value.find('}', i + 2)
if end == -1:
raise ValueError(f"Unclosed interpolation: {value}")
ref = value[i + 2:end]
if ':' in ref:
ref_parts = ref.split(':', 1)
ref_section = ref_parts[0]
ref_key = ref_parts[1].lower()
if ref_section not in sections:
raise KeyError(f"Section '{ref_section}' not found")
target_section = sections[ref_section]
if ref_key not in target_section:
raise KeyError(f"Key '{ref_key}' not found in section '{ref_section}'")
cycle_key = (ref_section, ref_key)
if cycle_key in visiting:
raise ValueError(f"Interpolation cycle detected: {ref_section}:{ref_key}")
visiting.add(cycle_key)
try:
substituted = _interpolate_value(
target_section[ref_key],
ref_section,
target_section,
visiting,
sections
)
finally:
visiting.discard(cycle_key)
result.append(substituted)
else:
ref_key = ref.lower()
if ref_key not in all_values:
raise KeyError(f"Key '{ref_key}' not found")
cycle_key = (section_name, ref_key)
if cycle_key in visiting:
raise ValueError(f"Interpolation cycle detected: {section_name}:{ref_key}")
visiting.add(cycle_key)
try:
substituted = _interpolate_value(
all_values[ref_key],
section_name,
all_values,
visiting,
sections
)
finally:
visiting.discard(cycle_key)
result.append(substituted)
i = end + 1
else:
result.append(value[i])
i += 1
return ''.join(result)
``` Coding: TTL LRU (hard) Worked
```python
from collections import OrderedDict
from typing import Any, Callable, Optional
class TTLCache:
def __init__(self, capacity: int, ttl: float, clock: Callable[[], float]):
self.capacity = capacity
self.ttl = ttl
self.clock = clock
self._cache: OrderedDict[Any, tuple[Any, float]] = OrderedDict()
self._hits = 0
self._misses = 0
self._evictions = 0
self._expirations = 0
def _remove_expired(self) -> int:
"""Remove all expired entries, return count of removed entries."""
now = self.clock()
expired_keys = [key for key, (value, expiry) in self._cache.items() if now >= expiry]
for key in expired_keys:
del self._cache[key]
self._expirations += 1
return len(expired_keys)
def put(self, key: Any, value: Any) -> None:
if self.capacity == 0:
return
now = self.clock()
expiry = now + self.ttl
if key in self._cache:
# Update existing entry: refresh expiry and recency
self._cache[key] = (value, expiry)
self._cache.move_to_end(key)
else:
# New entry: first remove expired entries
self._remove_expired()
# If still at capacity, evict LRU
if len(self._cache) >= self.capacity:
self._cache.popitem(last=False)
self._evictions += 1
# Insert new entry
self._cache[key] = (value, expiry)
def get(self, key: Any, default: Any = None) -> Any:
if self.capacity == 0:
self._misses += 1
return default
now = self.clock()
if key not in self._cache:
self._misses += 1
return default
value, expiry = self._cache[key]
if now >= expiry:
# Entry expired: remove and count as expiration + miss
del self._cache[key]
self._expirations += 1
self._misses += 1
return default
# Entry is valid: mark as MRU and count as hit
self._cache.move_to_end(key)
self._hits += 1
return value
def __len__(self) -> int:
if self.capacity == 0:
return 0
now = self.clock()
count = 0
for key, (value, expiry) in self._cache.items():
if now < expiry:
count += 1
return count
def stats(self) -> dict:
return {
"hits": self._hits,
"misses": self._misses,
"evictions": self._evictions,
"expirations": self._expirations
}
``` Coding: Parse duration Worked
```python
import re
def parse_duration(s: str) -> int:
# Strip leading and trailing whitespace
s = s.strip()
# Check for empty or blank string
if not s:
raise ValueError("Empty or blank string")
# Pattern to match a non-negative integer followed by a unit (h, m, s)
part_pattern = r'(\d+)([hmsHMS])'
# Find all number-unit pairs
matches = re.findall(part_pattern, s)
if not matches:
raise ValueError("No valid duration parts found")
# Verify that the entire string consists only of valid parts and whitespace
# Build a pattern that allows optional whitespace between parts
full_pattern = r'^\s*(\d+[hmsHMS]\s*)+$'
if not re.fullmatch(full_pattern, s):
raise ValueError("Invalid duration format")
# Track seen units and their order
seen_units = set()
unit_order = ['h', 'm', 's']
last_unit_index = -1
total_seconds = 0
for number_str, unit in matches:
unit_lower = unit.lower()
# Check for repeated units
if unit_lower in seen_units:
raise ValueError(f"Repeated unit: {unit}")
# Check unit order (must be h, then m, then s)
current_index = unit_order.index(unit_lower)
if current_index < last_unit_index:
raise ValueError(f"Units out of order: {unit}")
seen_units.add(unit_lower)
last_unit_index = current_index
# Convert to seconds
number = int(number_str)
if unit_lower == 'h':
total_seconds += number * 3600
elif unit_lower == 'm':
total_seconds += number * 60
elif unit_lower == 's':
total_seconds += number
return total_seconds
``` Coding: Summarize ranges Worked
```python
def summarize_ranges(nums: list[int]) -> str:
if not nums:
return ""
# Sort and remove duplicates
unique_nums = sorted(set(nums))
if not unique_nums:
return ""
result = []
start = unique_nums[0]
prev = unique_nums[0]
for curr in unique_nums[1:]:
if curr == prev + 1:
prev = curr
else:
# Process the completed run
if prev - start >= 2:
result.append(f"{start}..{prev}")
else:
result.extend(str(x) for x in range(start, prev + 1))
start = curr
prev = curr
# Process the final run
if prev - start >= 2:
result.append(f"{start}..{prev}")
else:
result.extend(str(x) for x in range(start, prev + 1))
return ",".join(result)
``` Coding: Top customers Worked
```python
from collections import defaultdict
def top_customers(rows: list[dict], n: int) -> list[tuple[str, float]]:
def parse_amount(amount_str):
if not isinstance(amount_str, str):
raise ValueError("Amount must be a string")
s = amount_str.strip()
if not s:
raise ValueError("Empty amount string")
is_negative = s.startswith('-')
s = s.lstrip('-')
s = s.replace('$', '').replace(',', '')
try:
value = float(s)
except ValueError:
raise ValueError(f"Cannot parse amount: {amount_str}")
return -value if is_negative else value
customer_totals = defaultdict(float)
customer_names = {}
for row in rows:
customer_name = row.get("customer", "")
if not customer_name or not str(customer_name).strip():
continue
trimmed_name = str(customer_name).strip()
lower_name = trimmed_name.lower()
if lower_name not in customer_names:
customer_names[lower_name] = trimmed_name
amount_str = row.get("amount", "")
try:
amount = parse_amount(amount_str)
except (ValueError, TypeError):
continue
customer_totals[lower_name] += amount
results = []
for lower_name, total in customer_totals.items():
name = customer_names[lower_name]
rounded_total = round(total, 2)
results.append((name, rounded_total))
results.sort(key=lambda x: (-x[1], x[0]))
return results[:n]
``` Coding: Slugify Worked
```python
import unicodedata
import re
def slugify(title: str, max_len: int = 50) -> str:
# 1. Transliterate accented characters to ASCII and drop any other non-ASCII characters
# NFKD normalization decomposes characters (e.g., 'é' -> 'e' + combining accent)
# Encoding to ASCII with 'ignore' removes the combining marks and any remaining non-ASCII
slug = unicodedata.normalize('NFKD', title)
slug = slug.encode('ascii', 'ignore').decode('ascii')
# 2. Lowercase
slug = slug.lower()
# 3. Replace every run of characters other than a-z and 0-9 with a single "-"
# and strip leading/trailing "-"
slug = re.sub(r'[^a-z0-9]+', '-', slug)
slug = slug.strip('-')
# 4. If the slug is longer than max_len, shorten it without cutting a word
if len(slug) <= max_len:
return slug
words = slug.split('-')
result_words = []
current_length = 0
for word in words:
# Calculate length including separator if not the first word
sep_len = 1 if result_words else 0
new_length = current_length + sep_len + len(word)
if new_length <= max_len:
result_words.append(word)
current_length = new_length
else:
break
if result_words:
final_slug = '-'.join(result_words)
else:
# If even the first word is longer than max_len, hard-cut it to max_len characters
if words:
final_slug = words[0][:max_len]
else:
final_slug = ""
# 5. The result never ends with "-"
final_slug = final_slug.rstrip('-')
return final_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_time = clock()
def _refill(self) -> None:
now = self._clock()
elapsed = now - self._last_refill_time
if elapsed > 0:
added = elapsed * self._refill_per_sec
self._tokens = min(self._capacity, self._tokens + added)
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) 100% right
{
"outcome": {
"full_refund": 0.0,
"partial_refund": 0.0,
"store_credit": 0.99,
"nothing": 0.01
},
"defective": {
"true": 0.01,
"false": 0.99
}
} Decisions: Incident matrix (hard) 100% right
{
"severity": {
"0": 0.005,
"1": 0.985,
"2": 0.005,
"3": 0.005
},
"page": {
"true": 0.01,
"false": 0.99
}
} Decisions: Tool followup (hard) 100% right
{
"tool": {
"web_search": 0.002,
"calculator": 0.003,
"calendar": 0.98,
"email": 0.005,
"none": 0.01
},
"confirm": {
"true": 0.95,
"false": 0.05
}
} Decisions: Legit security alert (hard) 100% right
{
"phishing": {
"true": 0.01,
"false": 0.99
},
"action_needed": {
"true": 0.02,
"false": 0.98
}
} 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": 0.99,
"false": 0.01
},
"support": {
"true": 0.99,
"false": 0.01
}
} Decisions: Support checkout down 100% right
{
"department": {
"billing": 0.1,
"technical": 0.9,
"account": 0.0,
"shipping": 0.0,
"sales": 0.0
},
"urgency": {
"0": 0.0,
"1": 0.0,
"2": 0.01,
"3": 0.99
},
"outage": {
"true": 0.99,
"false": 0.01
}
} Decisions: Refund wrong plan 100% right
{
"department": {
"billing": 0.99,
"technical": 0.0025,
"account": 0.0025,
"shipping": 0.0025,
"sales": 0.0025
},
"refund": {
"true": 0.99,
"false": 0.01
},
"tone": {
"frustrated": 0.01,
"calm": 0.99
}
} Decisions: Moderation doxxing 100% right
{
"policy": {
"none": 0.005,
"harassment": 0.99,
"hate": 0.002,
"spam": 0.002,
"self_harm": 0.001
},
"personal_info": {
"true": 0.995,
"false": 0.005
}
} Decisions: Route calendar 100% right
{
"tool": {
"web_search": 0.01,
"calculator": 0.00,
"calendar": 0.95,
"email": 0.03,
"none": 0.01
},
"confirm": {
"true": 0.98,
"false": 0.02
}
} Decisions: Doc invoice missing due 100% right
{
"doc_type": {
"invoice": 0.99,
"resume": 0.0,
"contract": 0.0,
"bank_statement": 0.0,
"other": 0.01
},
"missing_due_date": {
"true": 0.99,
"false": 0.01
}
} Decisions: Phishing paypal 100% right
{
"phishing": {
"true": 0.999,
"false": 0.001
},
"risk": {
"0": 0.001,
"1": 0.001,
"2": 0.001,
"3": 0.997
}
} Decisions: Pii ssn email 100% right
{
"data_kind": {
"none": 0.0,
"contact": 0.01,
"financial": 0.01,
"government_id": 0.98,
"health": 0.0
},
"sensitive": {
"true": 0.99,
"false": 0.01
}
} Decisions: Review mixed 100% right
{
"sentiment": {
"positive": 0.01,
"neutral": 0.01,
"negative": 0.98
},
"defect": {
"true": 0.99,
"false": 0.01
},
"recommend": {
"true": 0.01,
"false": 0.99
}
} Documents: Saas escalator (hard) 100% right
{
"year2_price_per_seat_month": 47.25,
"year3_price_per_seat_month": 47.25,
"year1_invoice": 58320.00,
"year2_invoice": 61236.00,
"addon_months_billed": 6,
"addon_invoice": 38556.00,
"year3_invoice": 134946.00,
"year3_discount_percent": 15,
"total_contract_value": 293058.00,
"contract_end_date": "2027-02-28"
} Documents: Expense thread 96% 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.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": 65.00,
"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.00,
"monthly_rent_from_2025_06_01": 2236.00,
"late_fee_from_2025_06_01": 111.80,
"security_deposit": 2150.00,
"total_pet_deposits": 800.00,
"total_monthly_payment_july_2025": 2306.00,
"move_in_payment": 4700.00
} 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: 28B parameters. First tested OCT 10.
Comments
Sign in with GitHub to comment. Spam and abuse are hidden automatically.