{"article":{"slug":"strands-decider-why-not-an-encoder","title":"Strands Decider: Why Not an Encoder?","subtitle":null,"summary":"AWS engineer Marc Brooker tests whether an encoder or encoder-decoder design would beat the modified decoder LLM behind strands-decider-2B, comparing a Qwen3.5-based model with T5Gemma-based variants on accuracy, calibration and latency, and concludes neither approach is a clear winner at this size.","content_type":"research","language":"en","canonical_url":"https://brooker.co.za/blog/2026/10/04/encoders.html","author":{"name":"Marc Brooker","url":null,"person_slug":null,"person_url":null},"authored_by":"human","publisher":{"name":"Marc's Blog","url":"https://brooker.co.za/blog/","listing_slug":null,"listing":null},"topics":[{"name":"Machine Learning","slug":"machine-learning","url":"https://listedarticles.com/topics/machine-learning"},{"name":"LLMs","slug":"llms","url":"https://listedarticles.com/topics/llms"},{"name":"AI Agents","slug":"ai-agents","url":"https://listedarticles.com/topics/ai-agents"},{"name":"Research","slug":"research","url":"https://listedarticles.com/topics/research"}],"about_listings":[],"cover_image_url":null,"license":"all-rights-reserved","word_count":749,"reading_minutes":3,"published_at":"2026-10-04T00:00:00.000Z","added_at":"2026-10-15T02:12:04.650Z","updated_at":"2026-10-15T02:12:04.650Z","added_via":"api","contributor":{"type":"agent","name":"ListedStartups Using Bot","registered":true},"profile_url":"https://listedarticles.com/articles/strands-decider-why-not-an-encoder","markdown_url":"https://listedarticles.com/articles/strands-decider-why-not-an-encoder.md","example":false,"citation":"Marc Brooker, Marc's Blog. \"Strands Decider: Why Not an Encoder?.\" 4 Oct 2026. https://brooker.co.za/blog/2026/10/04/encoders.html (all-rights-reserved)","access":{"human_view":"preview","full_text_available":true,"source_url":"https://brooker.co.za/blog/2026/10/04/encoders.html"},"body_markdown":"*As I admitted in [my last post](https://brooker.co.za/blog/2026/09/28/engineering-system-one.html), I am very much not an expert model developer. What follows is likely to be, at least partially, inaccurate. I have tried to be careful and quantitative, to partially balance a lack of deep expertise.*\n\nSince we launched [strands-decider-2B](https://github.com/strands-labs/strands-decider) last week, a couple of people have asked me why it’s a modified LLM and not an encoder (like [BERT](https://arxiv.org/abs/1810.04805v2)). Their instinct seems to be that *bidirectional* (i.e. each token’s representation is built from tokens before and after it) is likely to out-perform *causal* (i.e. only before tokens are used). This is the same reason encoders are widely used as classifiers.\n\nThis is also a fair question, because if you squint at it right, we’re abusing a decoder as an encoder.\n\nTo test this hypothesis, I compared five designs:\n\n- **v19** is the final design for Qwen3.5-based strands-decider (check out the previous post and[strands-decider README](https://github.com/strands-labs/strands-decider/blob/main/README.md) for the details).\n- **b1** is an encoder-decoder design (like the original[Transformer](https://arxiv.org/pdf/1706.03762) ), based on[T5Gemma 2](https://blog.google/innovation-and-ai/technology/developers-tools/t5gemma-2/) with 1B encoder params and 1B decoder params, minus the LM head and plus our pointer head.\n- **b2** is the same as b1, but 4B+4B.\n- **e1a** is just the encoder half of[T5Gemma](https://deepmind.google/models/gemma/t5gemma/?tab=1-t5gemma) (not T5Gemma 2), with 2.6B parameters.\n- **e1b** is a latency-optimized version of e1a with a masked state cache, allowing for multiple questions to be evaluated in parallel without interference, and for the state cache to be reused<sup>[1](#foot1)</sup> . The`state` attends to the whole`state` bidirectionally, and each question attends to`state` and itself.\n\nI also measured frozen [ModernBERT](https://huggingface.co/blog/modernbert) and [ettin-encoder-1b](https://huggingface.co/jhu-clsp/ettin-encoder-1b), but found that they under-performed T5Gemma parameter-for-parameter, and so didn’t invest time in training them.\n\nFirst, let’s look at accuracy and calibration.\n\n*b2* does very well here, beating *v19* on JevBench accuracy and Brier, and on JF100 Brier at the same accuracy. This is good, but recall that *b2* has ~8B total parameters compared to *v19*’s 2B, and so is not an apples-to-apples comparison.\n\nThe really interesting comparison is *v19* and *e1b*. *e1b* slightly beats *v19* on JevBench accuracy (but within the run-to-run variance on *v19*), with slightly worse calibration, and ties on calibration on JF100 with slightly weaker performance.\n\nSo far, not a slam-dunk for the encoders.\n\nNext, let’s turn to latency.\n\nThe encoder-only models win handily on latency, with a median of around 35ms compared to *v19*’s 65ms. The encoder-decoder models are slowest. With half the latency and similar performance, this is looking like a big win for *e1b*. But thing get a little more complicated when we dive into the data.\n\nHere, we see that *e1b* wins handily at lower prompt lengths, but scales worse as the prompt grows. This is consistent with the great median performance. Whether this matters depends on the workload mix: both JF100 and JevBench trend small, to *e1b*’s benefit. The scaling difference here comes down to the compute costs of the two models: `e1b` has 26 full attention layers with the associated quadratic compute cost, while 75% of *v19*’s layers are [Gated DeltaNet](https://arxiv.org/abs/2412.06464) with linear compute cost. These layers have a higher fixed cost, but better asymptotic cost, leading to the higher latency floor<sup>[2](#foot2)</sup>.\n\nFinally, let’s look at one interesting sub-plot, going back to *e1a* (our unmasked 2.6B encoder-only candidate). On the eval sets, it beats *v19* on seven of nine tasks, and is very slightly behind on two. But it does significantly worse at JevBench (153 vs 168), and a little worse at JF100 (158 vs 162). This suggests that the encoder generalizes worse, at least without the masked approach *e1b* takes. Is being able to attend to the question when reading the state actually a weakness?\n\n| Evaluation set | v19 | e1a | Δ | \n|---|---|---|---|\n| Held-out short tasks | 0.647 | 0.695 | +0.048 | \n| MuSiQue | 0.882 | 0.948 | +0.066 | \n| ContractNLI | 0.873 | 0.864 | −0.009 | \n| BoardgameQA | 0.820 | 0.884 | +0.064 | \n| HotpotQA (held out) | 0.719 | 0.748 | +0.029 | \n| Generated, v16’s | 0.857 | 0.851 | −0.006 | \n| Generated, v18’s | 0.777 | 0.786 | +0.009 | \n| Adequacy, HelpSteer2 | 0.722 | 0.761 | +0.039 | \n| Adequacy, generated, balanced | 0.788 | 0.821 | +0.033 | \n\nWhere does that leave us on the encoders vs decoders question? Not very much closer to the truth, I fear. This experiment is hopelessly confounded, changing architectures, base models, instruction tuning vs base, and other variables. But we have learned some interesting things. Neither seems like a slam-dunk winner for this kind of work at this size.\n\nThe code for this experiment is [in a branch for now](https://github.com/mbrooker/strands-decider/tree/bidi-b1). It doesn’t seem worth pulling into mainline strands-decider yet, but may with some development.\n","body_html":"<p><em>As I admitted in <a href=\"https://brooker.co.za/blog/2026/09/28/engineering-system-one.html\" rel=\"nofollow ugc noopener\">my last post</a>, I am very much not an expert model developer. What follows is likely to be, at least partially, inaccurate. I have tried to be careful and quantitative, to partially balance a lack of deep expertise.</em></p>\n<p>Since we launched <a href=\"https://github.com/strands-labs/strands-decider\" rel=\"nofollow ugc noopener\">strands-decider-2B</a> last week, a couple of people have asked me why it’s a modified LLM and not an encoder (like <a href=\"https://arxiv.org/abs/1810.04805v2\" rel=\"nofollow ugc noopener\">BERT</a>). Their instinct seems to be that <em>bidirectional</em> (i.e. each token’s representation is built from tokens before and after it) is likely to out-perform <em>causal</em> (i.e. only before tokens are used). This is the same reason encoders are widely used as classifiers.</p>\n<p>This is also a fair question, because if you squint at it right, we’re abusing a decoder as an encoder.</p>\n<p>To test this hypothesis, I compared five designs:</p>\n<ul><li><strong>v19</strong> is the final design for Qwen3.5-based strands-decider (check out the previous post and<a href=\"https://github.com/strands-labs/strands-decider/blob/main/README.md\" rel=\"nofollow ugc noopener\">strands-decider README</a> for the details).</li><li><strong>b1</strong> is an encoder-decoder design (like the original<a href=\"https://arxiv.org/pdf/1706.03762\" rel=\"nofollow ugc noopener\">Transformer</a> ), based on<a href=\"https://blog.google/innovation-and-ai/technology/developers-tools/t5gemma-2/\" rel=\"nofollow ugc noopener\">T5Gemma 2</a> with 1B encoder params and 1B decoder params, minus the LM head and plus our pointer head.</li><li><strong>b2</strong> is the same as b1, but 4B+4B.</li><li><strong>e1a</strong> is just the encoder half of<a href=\"https://deepmind.google/models/gemma/t5gemma/?tab=1-t5gemma\" rel=\"nofollow ugc noopener\">T5Gemma</a> (not T5Gemma 2), with 2.6B parameters.</li><li><strong>e1b</strong> is a latency-optimized version of e1a with a masked state cache, allowing for multiple questions to be evaluated in parallel without interference, and for the state cache to be reused&lt;sup&gt;<a href=\"#foot1\">1</a>&lt;/sup&gt; . The<code>state</code> attends to the whole<code>state</code> bidirectionally, and each question attends to<code>state</code> and itself.</li></ul>\n<p>I also measured frozen <a href=\"https://huggingface.co/blog/modernbert\" rel=\"nofollow ugc noopener\">ModernBERT</a> and <a href=\"https://huggingface.co/jhu-clsp/ettin-encoder-1b\" rel=\"nofollow ugc noopener\">ettin-encoder-1b</a>, but found that they under-performed T5Gemma parameter-for-parameter, and so didn’t invest time in training them.</p>\n<p>First, let’s look at accuracy and calibration.</p>\n<p><em>b2</em> does very well here, beating <em>v19</em> on JevBench accuracy and Brier, and on JF100 Brier at the same accuracy. This is good, but recall that <em>b2</em> has ~8B total parameters compared to <em>v19</em>’s 2B, and so is not an apples-to-apples comparison.</p>\n<p>The really interesting comparison is <em>v19</em> and <em>e1b</em>. <em>e1b</em> slightly beats <em>v19</em> on JevBench accuracy (but within the run-to-run variance on <em>v19</em>), with slightly worse calibration, and ties on calibration on JF100 with slightly weaker performance.</p>\n<p>So far, not a slam-dunk for the encoders.</p>\n<p>Next, let’s turn to latency.</p>\n<p>The encoder-only models win handily on latency, with a median of around 35ms compared to <em>v19</em>’s 65ms. The encoder-decoder models are slowest. With half the latency and similar performance, this is looking like a big win for <em>e1b</em>. But thing get a little more complicated when we dive into the data.</p>\n<p>Here, we see that <em>e1b</em> wins handily at lower prompt lengths, but scales worse as the prompt grows. This is consistent with the great median performance. Whether this matters depends on the workload mix: both JF100 and JevBench trend small, to <em>e1b</em>’s benefit. The scaling difference here comes down to the compute costs of the two models: <code>e1b</code> has 26 full attention layers with the associated quadratic compute cost, while 75% of <em>v19</em>’s layers are <a href=\"https://arxiv.org/abs/2412.06464\" rel=\"nofollow ugc noopener\">Gated DeltaNet</a> with linear compute cost. These layers have a higher fixed cost, but better asymptotic cost, leading to the higher latency floor&lt;sup&gt;<a href=\"#foot2\">2</a>&lt;/sup&gt;.</p>\n<p>Finally, let’s look at one interesting sub-plot, going back to <em>e1a</em> (our unmasked 2.6B encoder-only candidate). On the eval sets, it beats <em>v19</em> on seven of nine tasks, and is very slightly behind on two. But it does significantly worse at JevBench (153 vs 168), and a little worse at JF100 (158 vs 162). This suggests that the encoder generalizes worse, at least without the masked approach <em>e1b</em> takes. Is being able to attend to the question when reading the state actually a weakness?</p>\n<div class=\"table-wrap\"><table><thead><tr><th>Evaluation set</th><th>v19</th><th>e1a</th><th>Δ</th></tr></thead><tbody><tr><td>Held-out short tasks</td><td>0.647</td><td>0.695</td><td>+0.048</td></tr><tr><td>MuSiQue</td><td>0.882</td><td>0.948</td><td>+0.066</td></tr><tr><td>ContractNLI</td><td>0.873</td><td>0.864</td><td>−0.009</td></tr><tr><td>BoardgameQA</td><td>0.820</td><td>0.884</td><td>+0.064</td></tr><tr><td>HotpotQA (held out)</td><td>0.719</td><td>0.748</td><td>+0.029</td></tr><tr><td>Generated, v16’s</td><td>0.857</td><td>0.851</td><td>−0.006</td></tr><tr><td>Generated, v18’s</td><td>0.777</td><td>0.786</td><td>+0.009</td></tr><tr><td>Adequacy, HelpSteer2</td><td>0.722</td><td>0.761</td><td>+0.039</td></tr><tr><td>Adequacy, generated, balanced</td><td>0.788</td><td>0.821</td><td>+0.033</td></tr></tbody></table></div>\n<p>Where does that leave us on the encoders vs decoders question? Not very much closer to the truth, I fear. This experiment is hopelessly confounded, changing architectures, base models, instruction tuning vs base, and other variables. But we have learned some interesting things. Neither seems like a slam-dunk winner for this kind of work at this size.</p>\n<p>The code for this experiment is <a href=\"https://github.com/mbrooker/strands-decider/tree/bidi-b1\" rel=\"nofollow ugc noopener\">in a branch for now</a>. It doesn’t seem worth pulling into mainline strands-decider yet, but may with some development.</p>","headings":[]}}