Review · updated OCT 11

Qwen3.6 35B A3B review: a solid all-rounder

It scored 86 out of 100, #11 of 56. It solved 24 of 30 coding jobs and scored 93 on reading documents. Runs on a 24 GB graphics card or a Mac with 48 GB.

The short version
  • Qwen3.6 35B A3B 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: 86 out of 100, #11 of 56.
  • It solved 24 of 30 coding jobs and scored 93 on reading documents. On our hardest tasks it scored 73.
  • Runs on a 24 GB graphics card or a Mac with 48 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.6 35B A3B got 24 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.6 35B A3B scored 93; the best model scored 100.

TestScorePublic questionsSecret questions
Coding808678
Reading documents939492
Decisions9910099

On the 18 hardest tasks (included in the scores above) it scored 73. 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.6 35B 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 GBYesQ4
Graphics card, 32 GBYesQ6
Mac, 16 GBNo–
Mac, 24 GBNo–
Mac, 32 GBNo–
Mac, 48 GBYesQ6
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

Search for "Qwen3.6 35B A3B 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.6 35B 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: dict[str, dict[str, str]] = {}
    defaults: dict[str, str] = {}
    current_section: str | None = None
    last_key: str | None = None
    raw_lines = text.splitlines()

    i = 0
    while i < len(raw_lines):
        line = raw_lines[i]
        stripped = line.lstrip()

        # Rule 3: ignore empty or comment lines
        if not stripped or stripped[0] in (';', '#'):
            i += 1
            continue

        # Rule 6: continuation line check
        if line[0].isspace():
            if last_key is None:
                raise ValueError("Continuation line without preceding key line")
            if current_section is None:
                raise ValueError("Continuation line outside section")
            sections[current_section][last_key] += '\n' + stripped
            i += 1
            continue

        # Rule 1: Section header
        if stripped.startswith('[') and stripped.endswith(']'):
            sec_name = stripped[1:-1]
            current_section = sec_name
            if sec_name == 'DEFAULT':
                defaults.update(sections.get(sec_name, {}))
            else:
                sections[sec_name] = {}
            last_key = None
            i += 1
            continue

        # Rule 10: Key line before any section header
        if current_section is None:
            raise ValueError("Key line before any section header")

        # Rule 2: Parse key=value or key:value
        eq_pos = line.find('=')
        colon_pos = line.find(':')
        candidates = [p for p in (eq_pos, colon_pos) if p != -1]
        if not candidates:
            raise ValueError(f"Malformed line: {line}")

        split_idx = min(candidates)
        raw_key = line[:split_idx]
        raw_val = line[split_idx+1:]

        key = raw_key.strip().lower()
        if not key:
            raise ValueError(f"Empty key in line: {line}")

        # Process value
        val = raw_val.strip()
        if val.startswith('"'):
            end_quote = val.find('"', 1)
            if end_quote == -1:
                raise ValueError("Unclosed quote in value")
            content = val[1:end_quote]
            # Check for inline comment after closing quote
            rest = val[end_quote+1:]
            cmt_idx = -1
            for pat in (' ;', ' #'):
                i_cmt = rest.find(pat)
                if i_cmt != -1 and (cmt_idx == -1 or i_cmt < cmt_idx):
                    cmt_idx = i_cmt
            # If comment found, ignore text before it (typically whitespace)
            final_val = content
        else:
            cmt_idx = -1
            for pat in (' ;', ' #'):
                i_cmt = val.find(pat)
                if i_cmt != -1 and (cmt_idx == -1 or i_cmt < cmt_idx):
                    cmt_idx = i_cmt
            if cmt_idx != -1:
                val = val[:cmt_idx]
            final_val = val.rstrip()  # Rule 2: values stripped

        # Rule 7: later value wins
        sections[current_section][key] = final_val
        last_key = key
        i += 1

    # Rule 8: Merge defaults
    for sec in sections:
        for k, v in defaults.items():
            if k not in sections[sec]:
                sections[sec][k] = v

    # Rule 9: Interpolation
    ref_pattern = re.compile(r'\$\{([^}]+)\}')

    def resolve(sec: str, key: str, cache: dict, resolving: set) -> str:
        cache_key = (sec, key)
        if cache_key in cache:
            return cache[cache_key]
        if cache_key in resolving:
            raise ValueError("Interpolation cycle detected")
        resolving.add(cache_key)

        current_val = sections[sec][key]

        def replacer(match):
            ref = match.group(1)
            if ':' in ref:
                s_ref, k_ref = ref.split(':', 1)
                if s_ref not in sections:
                    raise KeyError(f"Section '{s_ref}' not found")
                if k_ref not in sections[s_ref]:
                    raise KeyError(f"Key '{k_ref}' not found in section '{s_ref}'")
                target_sec, target_key = s_ref, k_ref
            else:
                if ref not in sections[sec]:
                    raise KeyError(f"Key '{ref}' not found in section '{sec}'")
                target_sec, target_key = sec, ref
            return resolve(target_sec, target_key, cache, resolving)

        resolved_val = ref_pattern.sub(replacer, current_val)
        resolving.discard(cache_key)
        cache[cache_key] = resolved_val
        return resolved_val

    resolved_cache = {}
    for sec_name in list(sections.keys()):
        for key in list(sections[sec_name].keys()):
            sections[sec_name][key] = resolve(sec_name, key, resolved_cache, set())

    return sections
```
Coding: TTL LRU (hard) Worked


```python
from collections import OrderedDict
from typing import Any, Callable, Dict

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] = OrderedDict()
        self._stats = {"hits": 0, "misses": 0, "evictions": 0, "expirations": 0}

    def get(self, key: Any, default: Any = None) -> Any:
        now = self._clock()
        if key in self._cache:
            value, expiry = self._cache[key]
            if now >= expiry:
                # Entry has expired
                del self._cache[key]
                self._stats["expirations"] += 1
                self._stats["misses"] += 1
                return default
            
            # Entry is valid, mark as most recently used
            self._cache.move_to_end(key)
            self._stats["hits"] += 1
            return value
        else:
            self._stats["misses"] += 1
            return default

    def put(self, key: Any, value: Any) -> None:
        if self._capacity == 0:
            return

        now = self._clock()

        # If key exists, update value, refresh expiry, and move to MRU
        if key in self._cache:
            self._cache[key] = (value, now + self._ttl)
            self._cache.move_to_end(key)
            return

        # Key is new. We need to ensure space.
        
        # 1. Remove all expired entries
        # Collect keys to delete to avoid modifying dict during iteration
        expired_keys = [k for k, (v, exp) in self._cache.items() if now >= exp]
        for k in expired_keys:
            del self._cache[k]
            self._stats["expirations"] += 1

        # 2. If still at capacity, evict the Least Recently Used entry
        while len(self._cache) >= self._capacity:
            # popitem(last=False) removes the first item (LRU)
            self._cache.popitem(last=False)
            self._stats["evictions"] += 1

        # 3. Insert new item
        # OrderedDict adds new keys to the end (MRU) automatically
        self._cache[key] = (value, now + self._ttl)

    def __len__(self) -> int:
        now = self._clock()
        count = 0
        # Count entries that are not expired without modifying state or stats
        for _, expiry in self._cache.values():
            if now < expiry:
                count += 1
        return count

    def stats(self) -> Dict[str, int]:
        return dict(self._stats)
```
Coding: Parse duration Worked


```python
import re

def parse_duration(s: str) -> int:
    """
    Converts a duration string into a total number of seconds.
    
    Supported units: h (hours), m (minutes), s (seconds).
    Units are case-insensitive.
    Format: integer followed by unit.
    Whitespace allowed before, after, and between parts.
    Units must appear in order h, m, s and each at most once.
    
    Raises ValueError for invalid formats.
    """
    # Check for empty or blank strings
    if not s or not s.strip():
        raise ValueError("Input string is empty or blank")
    
    # Regex pattern explanation:
    # ^\s*               : Start of string, allow optional leading whitespace
    # (?:(\d+)h)?        : Optional hours component: digits followed immediately by 'h'
    # \s*                : Optional whitespace between parts
    # (?:(\d+)m)?        : Optional minutes component: digits followed immediately by 'm'
    # \s*                : Optional whitespace between parts
    # (?:(\d+)s)?        : Optional seconds component: digits followed immediately by 's'
    # \s*                : Optional trailing whitespace
    # $                  : End of string
    # re.IGNORECASE      : Units are case-insensitive
    #
    # Note: We strictly enforce no whitespace inside the integer-unit block (e.g., "1 h" is invalid)
    # based on the constraint that whitespace is allowed "between parts".
    pattern = r'^\s*(?:(\d+)h)?\s*(?:(\d+)m)?\s*(?:(\d+)s)?\s*$'
    
    match = re.fullmatch(pattern, s, re.IGNORECASE)
    
    if not match:
        raise ValueError(f"Invalid duration string: '{s}'")
    
    # Extract values, default to 0 if group is None (unit not present)
    hours = int(match.group(1)) if match.group(1) is not None else 0
    minutes = int(match.group(2)) if match.group(2) is not None else 0
    seconds = int(match.group(3)) if match.group(3) is not None else 0
    
    return hours * 3600 + minutes * 60 + seconds
```
Coding: Summarize ranges Worked


```python
def summarize_ranges(nums: list[int]) -> str:
    if not nums:
        return ""
    
    sorted_nums = sorted(set(nums))
    runs = []
    i = 0
    n = len(sorted_nums)
    
    while i < n:
        j = i
        while j + 1 < n and sorted_nums[j+1] == sorted_nums[j] + 1:
            j += 1
        runs.append((sorted_nums[i], sorted_nums[j]))
        i = j + 1
        
    parts = []
    for start, end in runs:
        if end - start >= 2:
            parts.append(f"{start}..{end}")
        else:
            parts.extend(str(x) for x in range(start, end + 1))
            
    return ",".join(parts)
```
Coding: Top customers Worked


```python
def top_customers(rows: list[dict], n: int) -> list[tuple[str, float]]:
    name_map: dict[str, str] = {}
    totals: dict[str, float] = {}

    for row in rows:
        cust_raw = row.get("customer")
        if not isinstance(cust_raw, str):
            continue
        trimmed = cust_raw.strip()
        if not trimmed:
            continue

        lower = trimmed.lower()
        if lower not in name_map:
            name_map[lower] = trimmed

        amt_raw = row.get("amount")
        if not isinstance(amt_raw, str):
            continue
        try:
            cleaned = amt_raw.strip().replace('$', '').replace(',', '')
            if not cleaned:
                continue
            amount = float(cleaned)
        except ValueError:
            continue

        totals[lower] = totals.get(lower, 0.0) + amount

    results = [(name_map[lower], round(total, 2)) for lower, total in totals.items()]
    results.sort(key=lambda x: (-x[1], x[0]))
    return results[:n]
```
Coding: Slugify Worked


```python
import re
import unicodedata

def slugify(title: str, max_len: int = 50) -> str:
    # 1 & 2: Transliterate accented characters to ASCII and lowercase
    text = unicodedata.normalize('NFKD', title).encode('ascii', 'ignore').decode('ascii').lower()
    
    # 3: Replace every run of characters other than a-z and 0-9 with a single '-', strip leading/trailing '-'
    text = re.sub(r'[^a-z0-9]+', '-', text).strip('-')
    
    if not text:
        return ''
        
    # 4: Shorten without cutting a word
    words = text.split('-')
    parts = []
    current_len = 0
    
    for word in words:
        if len(word) > max_len:
            # Hard-cut if even the first word exceeds max_len
            parts.append(word[:max_len])
            break
        sep_len = 1 if current_len > 0 else 0
        if current_len + len(word) + sep_len <= max_len:
            parts.append(word)
            current_len += len(word) + sep_len
        else:
            break
            
    # 5: Result never ends with '-'
    return '-'.join(parts)
```
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._tokens = float(capacity)
        self._last_refill_time = self.clock()

    def _refill(self, now: float) -> None:
        if now > self._last_refill_time:
            elapsed = now - self._last_refill_time
            self._tokens = min(self.capacity, self._tokens + elapsed * self.refill_per_sec)
            self._last_refill_time = now

    def allow(self, cost: float = 1) -> bool:
        now = self.clock()
        self._refill(now)
        
        if self._tokens >= cost:
            self._tokens -= cost
            return True
        return False

    @property
    def tokens(self) -> float:
        now = self.clock()
        self._refill(now)
        return self._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.01, "false": 0.99}, "action_needed": {"true": 0.01, "false": 0.99}}
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.01,
    "technical": 0.98,
    "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.995,
    "false": 0.005
  },
  "tone": {
    "calm": 0.99,
    "frustrated": 0.01
  }
}
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,
    "calculator": 0,
    "calendar": 1,
    "email": 0,
    "none": 0
  },
  "confirm": {
    "true": 1,
    "false": 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.99, "false": 0.01}, "risk": {"0": 0.0, "1": 0.0, "2": 0.0, "3": 1.0}}
Decisions: Pii ssn email 100% right


{
  "data_kind": {
    "none": 0.0,
    "contact": 0.03,
    "financial": 0.02,
    "government_id": 0.95,
    "health": 0.0
  },
  "sensitive": {
    "true": 0.99,
    "false": 0.01
  }
}
Decisions: Review mixed 100% right
{
  "sentiment": {
    "positive": 0.0,
    "neutral": 0.0,
    "negative": 1.0
  },
  "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": 58320.0,
  "year2_invoice": 99792.0,
  "addon_months_billed": 6,
  "addon_invoice": 38556.0,
  "year3_invoice": 135048.0,
  "year3_discount_percent": 15.0,
  "total_contract_value": 293160.0,
  "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 100% 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-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: 36B parameters. First tested OCT 10.

Models that scored about the same

Comments

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