<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
    <channel>
        <title>Paradigm Shift</title>
        <description>A personal blog to track my technical journey ...</description>      
        <link>http://toranbillups.com</link>
        <atom:link href="http://toranbillups.com/feed.xml" rel="self" type="application/rss+xml" />
        
            <item>
                <title>Embracing Disagreement</title>
                <description>&lt;p&gt;For years, I was that engineer who obsessed over the perfect abstraction, the cleanest architecture, the most elegant solution. I&apos;d spend hours refactoring code and building features that were interesting but not valuable. I was optimizing for everything except what mattered most: building products that people actually wanted.&lt;/p&gt;

&lt;p&gt;This shift from code-centric to impact-centric thinking doesn&apos;t happen naturally. It required unlearning years of training that taught me to optimize for technical excellence. It also required embracing those conversations that make us feel uncomfortable at times to question assumptions we&apos;ve held sacred.&lt;/p&gt;

&lt;p&gt;What I&apos;ve discovered is that the path from over confident engineer to builder is paved with disagreement. A very specific type of productive tension can be helpful for teams to confront what really matters.&lt;/p&gt;

&lt;p&gt;The trouble with teams is that they develop unspoken rules, values, and habits that settle in over time. Everyone knows &quot;this is how we do things here&quot; until someone new joins and suddenly there&apos;s friction. Maybe you&apos;ve been the newcomer who sees things differently, or perhaps you&apos;ve been on the team when a fresh perspective comes to shake things up. In that moment, there&apos;s a choice: Do we smooth over the differences and keep the peace, or do we lean into tension and discover what really matters?&lt;/p&gt;

&lt;p&gt;My goal here isn&apos;t to confirm what you already believe. Instead I plan to challenge you, to make you question the practices and assumptions that have defined your career. Because here&apos;s the truth: disagreement doesn&apos;t just make for better code—it makes for stronger teams and better products.&lt;/p&gt;

&lt;p&gt;What I&apos;ve learned is that there are three specific types of disagreement that, when embraced, become a competitive advantage.&lt;/p&gt;

&lt;h2&gt;Disagreement About Process&lt;/h2&gt;

&lt;p&gt;The first is disagreement about the process. Simply put, do less but more often.&lt;/p&gt;

&lt;p&gt;Before we dive in, I want to recommend the book &lt;a href=&quot;https://basecamp.com/shapeup&quot;&gt;Shape Up&lt;/a&gt;, because it marks a defining moment in my software career. This book did something for me that I know it can do for you. It provides clarity about scope and brought to light arguably the most important skill of the current age given advancements in tooling especially.&lt;/p&gt;

&lt;blockquote&gt;
  &lt;p&gt;If we had asked how long would it take to build Basecamp, we never would have got it done. But if we had asked instead, what is a version of chat, for example, that we could deliver by the end of the week? Well, that&apos;s something entirely different.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;So when I say do less, but do it more often, I&apos;m not talking about working fewer hours or &lt;a href=&quot;https://www.claude.com/product/claude-code&quot;&gt;Claude Code&lt;/a&gt;. I&apos;m talking about agreeing to deliver less. And if we agree to this individually and collectively, we not only move quicker by cutting scope, but we de-risk the project by reducing time to market.&lt;/p&gt;

&lt;p&gt;First you negotiate to the very essential core as the authors refer to it. Then you expand on this core over time until you reach a point where the effort outweighs the value people are getting from it. Consistent delivery allows you to find this sweet spot without the disappointment that&apos;s so rampant in our industry today.&lt;/p&gt;

&lt;p&gt;Here&apos;s where the disagreement comes. When you start proposing this approach, you&apos;ll meet resistance and it will likely come from other programmers. We&apos;re trained to be meticulous and critical about details without exception. We&apos;re taught that shortcuts lead to technical debt, that rushing leads to bugs, that proper process prevents problems. This mismatch will put you into disagreements regularly. Your fellow engineers will push back.&lt;/p&gt;

&lt;p&gt;Your instinct, like mine, might be to avoid that conflict and just go along with the existing process. But organizations move at the speed of trust. The consistency and frequency of delivery are valuable and offer the most effective way to build that trust. And this process helps you and the team build the muscle memory required for scope hammering, with the added bonus that it helps you get to market faster and validate your ideas and often your assumptions.&lt;/p&gt;

&lt;p&gt;I was working with a team that historically delivered less often. Our standups were very technical and involved vague terms that kept our non-technical people at a distance. In short, this team isolated itself from collaboration and shared business objectives. I knew we could win the trust of our leaders by showing working software instead of making excuses. I knew this would invite more collaboration and foster real innovation. I knew proposing this would put me in direct conflict with the existing team.&lt;/p&gt;

&lt;p&gt;So instead of debating pros and cons endlessly, I simply started doing the work and modeled it for the wider team. I certainly felt some tension, but when questioned, I pointed to our goals and made it clear I wasn&apos;t cutting corners but scope. I was intentionally choosing progress over perfection daily, and it was working.&lt;/p&gt;

&lt;p&gt;In just a few short weeks, our business leaders felt at home in these standups because the discussion was much more outcome oriented. And this new energy and engagement brought a much faster feedback loop that our team had never seen, and it naturally helped us surface answers to questions we&apos;d stumbled on just the day before.&lt;/p&gt;

&lt;p&gt;To my surprise, most of the engineers followed my example, and the team made this transition from surviving to thriving. The cultural shift was uncomfortable at times but pushing through it transformed this company, allowing us to deliver more value more often.&lt;/p&gt;

&lt;h2&gt;Disagreement About Details&lt;/h2&gt;

&lt;blockquote&gt;
  &lt;p&gt;Nearly all software development policies and processes exist to prevent software from being released.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;I &lt;a href=&quot;https://www.industriallogic.com/blog/faster-and-more-predictable/&quot;&gt;frame&lt;/a&gt; it this way because we&apos;re going to be looking at disagreement in the details. And if you break down a lot of our jobs day-to-day, it is truly just managing these details.&lt;/p&gt;

&lt;p&gt;What I&apos;ll be sharing here may challenge the very decision-making process that you have day-to-day. First, I want to start out by sharing the two different phases that show up in every project. You have the uphill work, where there&apos;s a lot of uncertainty and you&apos;re doing a lot of problem solving. Then there&apos;s the downhill work, where it&apos;s more about execution.&lt;/p&gt;

&lt;p&gt;Early in the project, the details don&apos;t carry as much weight. But over time, as things become more clear, more understood, they become more important. The trouble shows up when we apply equal weight to every possible sharp edge and detail that we encounter. We rarely take the time required to consider how important one detail is versus another. So we just pile on the scope, which ultimately delays delivery.&lt;/p&gt;

&lt;p&gt;This is where the daily demo comes in handy. Simply put, you&apos;re going to show the previous day&apos;s work. And the time pressure you feel to deliver frequently like this provides the forcing function required to cut scope aggressively. This magnifies the details that matter most and helps the team minimize a focus on edge cases that can be tackled another day.&lt;/p&gt;

&lt;p&gt;The added bonus here of the daily demo is that working software quickly provides the clarity to set expectations. During these demos, make it a habit to encourage feedback from everyone involved. It doesn&apos;t mean that you commit to everything, but it does create this amazing feedback loop. And you do get a ton of value as a team here surfacing, not only problems, but often solutions.&lt;/p&gt;

&lt;p&gt;And this is when those disagreements begin to surface. This style of work often conflicts with more traditional roles and even the reward systems that optimize for code above all else. Remind everyone that incremental investment leads to monumental advancement. We&apos;re not shooting for a big bang release. This is truly working lean.&lt;/p&gt;

&lt;p&gt;One of the challenges with working lean is that decision-making day-to-day just looks radically different. One of the benefits though, is disagreement at this stage helps your team arrive at done more quickly by avoiding gold plating and bike shedding that run rampant when unchecked. The added bonus of course is you find technical issues faster because you&apos;re deploying more often. And equally important, you might flush out gaps in your domain knowledge by getting this in front of real stakeholders sooner.&lt;/p&gt;

&lt;p&gt;Recently, I went through a code review and I was using just a dictionary instead of a type class in Python. During this review that was flagged and the engineer said this would be an improvement. And this reminded me that if we optimize for code on our teams and in our organizations, we&apos;re going to get more time in the code. It&apos;s only when you optimize for impact that you consider other factors like speed to market, learning, or simply return on investment.&lt;/p&gt;

&lt;p&gt;Now the individual wasn&apos;t doing anything out of the ordinary. In fact, most of us are actually rewarded here to be champions of code above all else. And one of the challenges you&apos;re going to face here is that if the team is not aligned about what matters most, technical people, well, they&apos;ll do technical things but never lose sight of the impact.&lt;/p&gt;

&lt;p&gt;Now this disagreement actually gave us an opportunity to discuss the real impact and investment of time. So we talked about whether this improvement was necessary for the current iteration and whether we could wait until we validated this feature with real users.&lt;/p&gt;

&lt;p&gt;Now, I want to be clear. If you&apos;re working in a classic reward system, you may not always win these arguments. That&apos;s okay. Because you still shared something very important that technical people often miss, and that is the distinction between progress and perfection.&lt;/p&gt;

&lt;h2&gt;Disagreement About Purpose&lt;/h2&gt;

&lt;p&gt;For the final section here, we&apos;re going to talk about purpose and disagreement there. I recommend this book to you called &lt;a href=&quot;https://mattlemay.com/&quot;&gt;Impact First Product Teams&lt;/a&gt; by Matt LeMay. In it, you will find the most practical career advice for someone writing software in 2025. It includes a call for action that will be difficult for most of us, but one that is necessary to grasp as we enter a future where the market is very different than it has been for most of my lifetime working in tech.&lt;/p&gt;

&lt;p&gt;If nobody else is talking about it, ask about the highest impact work you can do. Avoid what Matt calls the &quot;low impact death spiral.&quot; This is easy, but often not helpful work that product teams get trapped in.&lt;/p&gt;

&lt;p&gt;Now there&apos;s a vast gap between the work and the impact, and we fill this with useful things like strategy, discovery, scoping, etc. This is often helpful, but the key here is throughout we should be making decisions with impact in view. But so often, the impact feels far away and the technical details are all too familiar, comfortable, and tangible.&lt;/p&gt;

&lt;p&gt;And this is the deepest level of disagreement, the most challenging for teams, honestly: questioning not just how we do the work, but applying real judgment about what work we choose to do at all.&lt;/p&gt;

&lt;p&gt;Something happened during our quarterly onsite that put our team&apos;s impact front and center. I put together a team building activity that essentially said, come up with the money to cover the team&apos;s cost each year, or we&apos;ll close up shop. Suddenly, every idea was on the table. This constraint stripped away all the comfortable busy work and forced us into difficult conversations.&lt;/p&gt;

&lt;p&gt;Should we focus on saving money or making money? This, of course, divided the group initially. What features actually drive revenue versus features that are interesting? What problems are we solving that people will actually pay for?&lt;/p&gt;

&lt;p&gt;After lots of refinement, thinking, and working through the problem and solution pairs, we landed on a single solution that everyone was energized about. The outcome was a clear, high-impact project with real potential to increase our revenue. And this type of disagreement is not only necessary, but critical for the longevity of the business. Without it, we choose to tackle low-risk and often low-value work that does little to move the business forward.&lt;/p&gt;

&lt;h2&gt;Your Competitive Advantage&lt;/h2&gt;

&lt;p&gt;My hope is that you will be set apart, not by following my advice or conventional wisdom from companies like Meta or Google, but by identifying those opportunities hidden in disagreement and turning them into your strategic advantage.&lt;/p&gt;

&lt;p&gt;Here&apos;s the key insight: &lt;strong&gt;Humility is the prerequisite for productive disagreement&lt;/strong&gt; because humility allows you to cut scope despite your intuition, ship imperfect code despite your instincts, and question the fundamental assumptions to surface truly high-impact work. With a measure of humility, this will bring your teams closer to impact, however that looks in your organization.&lt;/p&gt;

&lt;p&gt;Remember, that lens you bring to the team is really important. It becomes a problem when you think it&apos;s the most important.&lt;/p&gt;

&lt;p&gt;So disagreements are out there, and they&apos;re coming for you, whether you embrace them or not. The question is: will you see it as a burden or a competitive advantage?&lt;/p&gt;
</description>
                <pubDate>Sat, 04 Oct 2025 01:00:00 +0000</pubDate>
                <link>http://toranbillups.com/blog/archive/2025/10/04/embracing-disagreement/</link>
                <guid isPermaLink="true">http://toranbillups.com/blog/archive/2025/10/04/embracing-disagreement/</guid>
            </item>
        
            <item>
                <title>Building BM25 Keyword Search In Postgres</title>
                <description>&lt;style&gt;
    .benchmarks {
        background: white;
        padding: 30px;
        padding-bottom: 5px;
        border-radius: 8px;
        box-shadow: 0 2px 8px rgba(0,0,0,0.1);
    }
    .metric-group {
        margin-bottom: 35px;
    }
    .metric-label {
        font-weight: 600;
        color: #333;
        margin-bottom: 8px;
        font-size: 14px;
    }
    .bars {
        display: flex;
        gap: 10px;
        margin-bottom: 5px;
    }
    .bar {
        height: 30px;
        display: flex;
        align-items: center;
        padding: 0 10px;
        color: white;
        font-weight: 600;
        font-size: 13px;
        border-radius: 4px;
        transition: transform 0.2s;
    }
    .bar:hover {
        transform: translateX(2px);
    }
    .bm25 {
        background: #3b82f6;
    }
    .ilike {
        background: #10b981;
    }
    .legend {
        display: flex;
        gap: 20px;
        margin-top: 30px;
        padding-top: 20px;
        border-top: 1px solid #e5e5e5;
    }
    .legend-item {
        display: flex;
        align-items: center;
        gap: 8px;
        font-size: 14px;
    }
    .legend-color {
        width: 20px;
        height: 20px;
        border-radius: 3px;
    }
    .improvement {
        color: #059669;
        font-size: 13px;
        font-weight: 600;
        margin-top: 3px;
    }
&lt;/style&gt;

&lt;p&gt;It turns out, you can build a surprisingly powerful keyword search, known as BM25, with just a handful of SQL functions. What follows is my journey including what I&apos;ve learned, how this algorithm works in simple terms, and how I implemented it without any external dependencies.&lt;/p&gt;

&lt;h3&gt;The Foundation: From TF-IDF to BM25&lt;/h3&gt;

&lt;p&gt;Before diving into BM25, it&apos;s helpful to understand TF-IDF (Term Frequency-Inverse Document Frequency). It&apos;s a simple but powerful idea that helps determine the importance of a word per document within a wider collection of documents.&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;Term Frequency (TF):&lt;/strong&gt; This is just what it sounds like. How often does a term appear in a single document? The intuition is that if the word &quot;data&quot; appears 5 times in a 100-word article, it&apos;s probably more relevant to that article than a word that appears only once in that same document.&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Inverse Document Frequency (IDF):&lt;/strong&gt; This measures how rare a word is across *all* documents. Common words like &quot;the&quot; or &quot;a&quot; will appear everywhere and have a low IDF score, effectively penalizing them. Rare words will appear in fewer documents and should have a higher IDF score, signaling that they are more significant.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;By multiplying these two scores, you get the TF-IDF score, which gives you a weighted value for how important a word is to a specific document. This was the starting point for my exploration.&lt;/p&gt;

&lt;p&gt;BM25 is essentially a more refined version of this. It builds on the core concepts of TF-IDF but adds a couple of clever improvements:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;Term Frequency Saturation:&lt;/strong&gt; BM25 recognizes that a word&apos;s relevance doesn&apos;t increase linearly. The difference between a word appearing zero times and one time is huge. The difference between it appearing 20 times and 21 times is negligible. BM25 provides a simple configuration so you can control how quickly the term frequency score &quot;saturates.&quot;&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Document Length Normalization:&lt;/strong&gt; Longer documents naturally have an advantage because they have more opportunities for a search term to appear. BM25 provides a simple configuration so you can penalize longer documents to level the playing field, ensuring that relevance isn&apos;t just a side effect of verbosity.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;The Implementation&lt;/h3&gt;

&lt;p&gt;The beauty of this approach is that the entire search algorithm lives within Postgres and can be called from any tech stack.&lt;/p&gt;

&lt;p&gt;Here&apos;s a breakdown of the core components:&lt;/p&gt;

&lt;h4&gt;Text Processing and Tokenization&lt;/h4&gt;

&lt;p&gt;The first step in any search system is to break down raw text into meaningful terms, or tokens. I created a tokenize_and_count function to handle this. It takes a block of text and converts it to lowercase, removes common stop words using the built-in English stop words list (like &apos;a&apos;, &apos;the&apos;, &apos;is&apos;), uses stemming to reduce words to their root form (e.g., &quot;canceling&quot; and &quot;canceled&quot; both become &quot;cancel&quot;), filters out blank tokens, and finally returns a table of unique terms and their frequencies in the text.&lt;/p&gt;

&lt;div class=&quot;highlight&quot; data-language=&quot;elixir&quot;&gt;
  &lt;pre class=&quot;language-elixir&quot;&gt;
    &lt;code class=&quot;language-elixir&quot;&gt;CREATE OR REPLACE FUNCTION tokenize_and_count(input_text TEXT)&lt;/code&gt;
    &lt;code class=&quot;language-elixir&quot;&gt;RETURNS TABLE (term TEXT, count INTEGER) AS $$&lt;/code&gt;
    &lt;code class=&quot;language-elixir&quot;&gt;BEGIN&lt;/code&gt;
    &lt;code class=&quot;language-elixir&quot;&gt;    -- Get stemmed tokens with stop words removed&lt;/code&gt;
    &lt;code class=&quot;language-elixir&quot;&gt;    RETURN QUERY&lt;/code&gt;
    &lt;code class=&quot;language-elixir&quot;&gt;    WITH tokens AS (&lt;/code&gt;
    &lt;code class=&quot;language-elixir&quot;&gt;        SELECT word&lt;/code&gt;
    &lt;code class=&quot;language-elixir&quot;&gt;        FROM ts_parse(&apos;default&apos;, lower(input_text)) AS t(tokid, word)&lt;/code&gt;
    &lt;code class=&quot;language-elixir&quot;&gt;        WHERE tokid != 12  -- Filter out blank tokens&lt;/code&gt;
    &lt;code class=&quot;language-elixir&quot;&gt;    ),&lt;/code&gt;
    &lt;code class=&quot;language-elixir&quot;&gt;    processed_tokens AS (&lt;/code&gt;
    &lt;code class=&quot;language-elixir&quot;&gt;        SELECT&lt;/code&gt;
    &lt;code class=&quot;language-elixir&quot;&gt;            CASE&lt;/code&gt;
    &lt;code class=&quot;language-elixir&quot;&gt;                WHEN ts_lexize(&apos;public.simple_dict&apos;, word) = &apos;{}&apos;&lt;/code&gt;
    &lt;code class=&quot;language-elixir&quot;&gt;                THEN NULL  -- It&apos;s a stop word&lt;/code&gt;
    &lt;code class=&quot;language-elixir&quot;&gt;                ELSE COALESCE(&lt;/code&gt;
    &lt;code class=&quot;language-elixir&quot;&gt;                    (ts_lexize(&apos;public.english_stem&apos;, word))[1],&lt;/code&gt;
    &lt;code class=&quot;language-elixir&quot;&gt;                    word&lt;/code&gt;
    &lt;code class=&quot;language-elixir&quot;&gt;                )&lt;/code&gt;
    &lt;code class=&quot;language-elixir&quot;&gt;            END AS processed_word&lt;/code&gt;
    &lt;code class=&quot;language-elixir&quot;&gt;        FROM tokens&lt;/code&gt;
    &lt;code class=&quot;language-elixir&quot;&gt;    )&lt;/code&gt;
    &lt;code class=&quot;language-elixir&quot;&gt;    SELECT&lt;/code&gt;
    &lt;code class=&quot;language-elixir&quot;&gt;        processed_word as term,&lt;/code&gt;
    &lt;code class=&quot;language-elixir&quot;&gt;        COUNT(*)::INTEGER&lt;/code&gt;
    &lt;code class=&quot;language-elixir&quot;&gt;    FROM processed_tokens&lt;/code&gt;
    &lt;code class=&quot;language-elixir&quot;&gt;    WHERE processed_word IS NOT NULL&lt;/code&gt;
    &lt;code class=&quot;language-elixir&quot;&gt;    GROUP BY processed_word;&lt;/code&gt;
    &lt;code class=&quot;language-elixir&quot;&gt;END;&lt;/code&gt;
    &lt;code class=&quot;language-elixir&quot;&gt;$$ LANGUAGE plpgsql;&lt;/code&gt;
  &lt;/pre&gt;
&lt;/div&gt;

&lt;h4&gt;Storing Statistics for Performance&lt;/h4&gt;

&lt;p&gt;Calculating these scores on the fly for every document during every search would be incredibly slow. To optimize this, I created two key tables:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;code&gt;verse_stats&lt;/code&gt;: This table stores the pre-calculated term frequencies for each document. It holds the document&apos;s ID, its total length, and a JSONB object mapping each term to its count.&lt;/li&gt;
  &lt;li&gt;&lt;code&gt;term_stats&lt;/code&gt;: This table stores global statistics about each term, such as how many documents it appears in. This is crucial for calculating the IDF score efficiently.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I also created a materialized view, global_stats, to keep track of the total number of documents and the average document length across the entire corpus, which is needed for the BM25 calculation.&lt;/p&gt;

&lt;h4&gt;The BM25 Scoring Function&lt;/h4&gt;

&lt;p&gt;With the data structures in place, I implemented the core BM25 logic. The calculate_idf function uses the BM25 formula to determine a term&apos;s weight. It&apos;s slightly more robust than the standard TF-IDF version to ensure scores are non-negative.&lt;/p&gt;

&lt;div class=&quot;highlight&quot; data-language=&quot;elixir&quot;&gt;
  &lt;pre class=&quot;language-elixir&quot;&gt;
    &lt;code class=&quot;language-elixir&quot;&gt;CREATE OR REPLACE FUNCTION calculate_idf(term_doc_count INTEGER, total_docs INTEGER)&lt;/code&gt;
    &lt;code class=&quot;language-elixir&quot;&gt;RETURNS FLOAT AS $$&lt;/code&gt;
    &lt;code class=&quot;language-elixir&quot;&gt;BEGIN&lt;/code&gt;
    &lt;code class=&quot;language-elixir&quot;&gt;  -- Modified BM25 IDF formula to ensure non-negative scores&lt;/code&gt;
    &lt;code class=&quot;language-elixir&quot;&gt;  RETURN ln(1 + (total_docs - term_doc_count + 0.5) /&lt;/code&gt;
    &lt;code class=&quot;language-elixir&quot;&gt;                (term_doc_count + 0.5));&lt;/code&gt;
    &lt;code class=&quot;language-elixir&quot;&gt;END;&lt;/code&gt;
    &lt;code class=&quot;language-elixir&quot;&gt;$$ LANGUAGE plpgsql;&lt;/code&gt;
  &lt;/pre&gt;
&lt;/div&gt;

&lt;p&gt;The bm25_term_score function brings it all together, combining the term frequency, document length, and IDF score to produce the final score for a single term in a single document.&lt;/p&gt;

&lt;h4&gt;Putting it all Together&lt;/h4&gt;

&lt;p&gt;The final piece is the search_verses function. This function orchestrates the entire process: it takes a user&apos;s query text, tokenizes it, and to handle typos, it uses the pg_trgm extension to find terms in term_stats that are similar to the (potentially misspelled) query terms. It then joins the query terms against the verse_stats and term_stats tables, calculates the bm25_term_score for each matching term in each document, sums them up to get a final relevance score, and returns the top results.&lt;/p&gt;

&lt;div class=&quot;highlight&quot; data-language=&quot;elixir&quot;&gt;
  &lt;pre class=&quot;language-elixir&quot;&gt;
    &lt;code class=&quot;language-elixir&quot;&gt;CREATE OR REPLACE FUNCTION search_verses(&lt;/code&gt;
    &lt;code class=&quot;language-elixir&quot;&gt;    query_text TEXT,&lt;/code&gt;
    &lt;code class=&quot;language-elixir&quot;&gt;    k1 FLOAT DEFAULT 1.2,&lt;/code&gt;
    &lt;code class=&quot;language-elixir&quot;&gt;    b FLOAT DEFAULT 0.75,&lt;/code&gt;
    &lt;code class=&quot;language-elixir&quot;&gt;    limit_val INTEGER DEFAULT 10,&lt;/code&gt;
    &lt;code class=&quot;language-elixir&quot;&gt;    similarity_threshold FLOAT DEFAULT 0.3&lt;/code&gt;
    &lt;code class=&quot;language-elixir&quot;&gt;) RETURNS TABLE (&lt;/code&gt;
    &lt;code class=&quot;language-elixir&quot;&gt;    verse_id BIGINT,&lt;/code&gt;
    &lt;code class=&quot;language-elixir&quot;&gt;    score FLOAT,&lt;/code&gt;
    &lt;code class=&quot;language-elixir&quot;&gt;    content TEXT&lt;/code&gt;
    &lt;code class=&quot;language-elixir&quot;&gt;) AS $$&lt;/code&gt;
    &lt;code class=&quot;language-elixir&quot;&gt;DECLARE&lt;/code&gt;
    &lt;code class=&quot;language-elixir&quot;&gt;    v_total_docs INTEGER;&lt;/code&gt;
    &lt;code class=&quot;language-elixir&quot;&gt;    v_avg_length FLOAT;&lt;/code&gt;
    &lt;code class=&quot;language-elixir&quot;&gt;BEGIN&lt;/code&gt;
    &lt;code class=&quot;language-elixir&quot;&gt;    SELECT gs.total_docs, gs.avg_length&lt;/code&gt;
    &lt;code class=&quot;language-elixir&quot;&gt;    INTO v_total_docs, v_avg_length&lt;/code&gt;
    &lt;code class=&quot;language-elixir&quot;&gt;    FROM global_stats gs;&lt;/code&gt;
    &lt;code class=&quot;language-elixir&quot;&gt;    &lt;/code&gt;
    &lt;code class=&quot;language-elixir&quot;&gt;    RETURN QUERY&lt;/code&gt;
    &lt;code class=&quot;language-elixir&quot;&gt;    WITH raw_query_terms AS (&lt;/code&gt;
    &lt;code class=&quot;language-elixir&quot;&gt;        SELECT term&lt;/code&gt;
    &lt;code class=&quot;language-elixir&quot;&gt;        FROM tokenize_and_count(query_text)&lt;/code&gt;
    &lt;code class=&quot;language-elixir&quot;&gt;    ),&lt;/code&gt;
    &lt;code class=&quot;language-elixir&quot;&gt;    query_terms AS (&lt;/code&gt;
    &lt;code class=&quot;language-elixir&quot;&gt;        SELECT DISTINCT corrected_term AS term&lt;/code&gt;
    &lt;code class=&quot;language-elixir&quot;&gt;        FROM raw_query_terms rqt&lt;/code&gt;
    &lt;code class=&quot;language-elixir&quot;&gt;        CROSS JOIN LATERAL (&lt;/code&gt;
    &lt;code class=&quot;language-elixir&quot;&gt;            SELECT ts.term AS corrected_term&lt;/code&gt;
    &lt;code class=&quot;language-elixir&quot;&gt;            FROM term_stats ts&lt;/code&gt;
    &lt;code class=&quot;language-elixir&quot;&gt;            WHERE similarity(rqt.term, ts.term) &gt;= similarity_threshold&lt;/code&gt;
    &lt;code class=&quot;language-elixir&quot;&gt;            ORDER BY similarity(rqt.term, ts.term) DESC&lt;/code&gt;
    &lt;code class=&quot;language-elixir&quot;&gt;            LIMIT 1&lt;/code&gt;
    &lt;code class=&quot;language-elixir&quot;&gt;        ) AS best_match&lt;/code&gt;
    &lt;code class=&quot;language-elixir&quot;&gt;    ),&lt;/code&gt;
    &lt;code class=&quot;language-elixir&quot;&gt;    term_scores AS (&lt;/code&gt;
    &lt;code class=&quot;language-elixir&quot;&gt;        SELECT&lt;/code&gt;
    &lt;code class=&quot;language-elixir&quot;&gt;            d.verse_id,&lt;/code&gt;
    &lt;code class=&quot;language-elixir&quot;&gt;            bm25_term_score(&lt;/code&gt;
    &lt;code class=&quot;language-elixir&quot;&gt;                (d.terms-&gt;&gt;t.term)::INTEGER,&lt;/code&gt;
    &lt;code class=&quot;language-elixir&quot;&gt;                d.length,&lt;/code&gt;
    &lt;code class=&quot;language-elixir&quot;&gt;                calculate_idf(ts.doc_count, v_total_docs),&lt;/code&gt;
    &lt;code class=&quot;language-elixir&quot;&gt;                v_avg_length,&lt;/code&gt;
    &lt;code class=&quot;language-elixir&quot;&gt;                k1,&lt;/code&gt;
    &lt;code class=&quot;language-elixir&quot;&gt;                b&lt;/code&gt;
    &lt;code class=&quot;language-elixir&quot;&gt;            ) AS term_score,&lt;/code&gt;
    &lt;code class=&quot;language-elixir&quot;&gt;            v.text AS doc_text&lt;/code&gt;
    &lt;code class=&quot;language-elixir&quot;&gt;        FROM&lt;/code&gt;
    &lt;code class=&quot;language-elixir&quot;&gt;            verse_stats d&lt;/code&gt;
    &lt;code class=&quot;language-elixir&quot;&gt;        JOIN&lt;/code&gt;
    &lt;code class=&quot;language-elixir&quot;&gt;            verses v ON v.id = d.verse_id&lt;/code&gt;
    &lt;code class=&quot;language-elixir&quot;&gt;        JOIN&lt;/code&gt;
    &lt;code class=&quot;language-elixir&quot;&gt;            query_terms t ON d.terms ? t.term&lt;/code&gt;
    &lt;code class=&quot;language-elixir&quot;&gt;        JOIN&lt;/code&gt;
    &lt;code class=&quot;language-elixir&quot;&gt;            term_stats ts ON ts.term = t.term&lt;/code&gt;
    &lt;code class=&quot;language-elixir&quot;&gt;    )&lt;/code&gt;
    &lt;code class=&quot;language-elixir&quot;&gt;    SELECT&lt;/code&gt;
    &lt;code class=&quot;language-elixir&quot;&gt;        ts.verse_id,&lt;/code&gt;
    &lt;code class=&quot;language-elixir&quot;&gt;        SUM(ts.term_score) AS score,&lt;/code&gt;
    &lt;code class=&quot;language-elixir&quot;&gt;        ts.doc_text AS content&lt;/code&gt;
    &lt;code class=&quot;language-elixir&quot;&gt;    FROM&lt;/code&gt;
    &lt;code class=&quot;language-elixir&quot;&gt;        term_scores ts&lt;/code&gt;
    &lt;code class=&quot;language-elixir&quot;&gt;    GROUP BY&lt;/code&gt;
    &lt;code class=&quot;language-elixir&quot;&gt;        ts.verse_id,&lt;/code&gt;
    &lt;code class=&quot;language-elixir&quot;&gt;        ts.doc_text&lt;/code&gt;
    &lt;code class=&quot;language-elixir&quot;&gt;    ORDER BY&lt;/code&gt;
    &lt;code class=&quot;language-elixir&quot;&gt;        score DESC&lt;/code&gt;
    &lt;code class=&quot;language-elixir&quot;&gt;    LIMIT limit_val;&lt;/code&gt;
    &lt;code class=&quot;language-elixir&quot;&gt;END;&lt;/code&gt;
    &lt;code class=&quot;language-elixir&quot;&gt;$$ LANGUAGE plpgsql;&lt;/code&gt;
  &lt;/pre&gt;
&lt;/div&gt;

&lt;h4&gt;Highlights&lt;/h4&gt;

&lt;p&gt;With everything fully operational I decided to summarize what I&apos;ve done and benchmark this against something much simpler to get a feel for how this relevance scoring worked in practice.&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;BM25 Ranking:&lt;/strong&gt; Industry-standard relevance scoring with proper IDF calculation and document length normalization&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;PostgreSQL-Native:&lt;/strong&gt; Zero external dependencies, using built-in text search and JSONB&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Fuzzy Matching:&lt;/strong&gt; Trigram-based typo correction with configurable similarity threshold&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Text Analysis:&lt;/strong&gt; Complete pipeline with tokenization, stemming and stopword removal&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Incremental Updates:&lt;/strong&gt; Efficient document re-indexing with proper term statistics maintenance&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Optimized Storage:&lt;/strong&gt; JSONB term frequencies with GIN indexing for fast lookups&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Materialized Statistics:&lt;/strong&gt; Pre-computed global metrics for efficient scoring&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;To benchmark this I wrote a simple &lt;a href=&quot;https://github.com/toranb/encoder-search/blob/main/beir.py&quot;&gt;BEIR&lt;/a&gt; script that showed a clear improvement in relevance ranking.&lt;/p&gt;

&lt;div class=&quot;benchmarks&quot;&gt;
    &lt;div class=&quot;metric-group&quot;&gt;
        &lt;div class=&quot;metric-label&quot;&gt;NDCG@10 (relevance)&lt;/div&gt;
        &lt;div class=&quot;bars&quot;&gt;
            &lt;div class=&quot;bar bm25&quot; style=&quot;width: 100%;&quot;&gt;BM25: 0.2940&lt;/div&gt;
        &lt;/div&gt;
        &lt;div class=&quot;bars&quot;&gt;
            &lt;div class=&quot;bar ilike&quot; style=&quot;width: 37.6%;&quot;&gt;ILIKE: 0.1105&lt;/div&gt;
        &lt;/div&gt;
        &lt;div class=&quot;improvement&quot;&gt;BM25 leads by +166.1%&lt;/div&gt;
    &lt;/div&gt;

    &lt;div class=&quot;metric-group&quot;&gt;
        &lt;div class=&quot;metric-label&quot;&gt;Precision@10 (accuracy of the top 10 results)&lt;/div&gt;
        &lt;div class=&quot;bars&quot;&gt;
            &lt;div class=&quot;bar bm25&quot; style=&quot;width: 100%;&quot;&gt;BM25: 0.2806&lt;/div&gt;
        &lt;/div&gt;
        &lt;div class=&quot;bars&quot;&gt;
            &lt;div class=&quot;bar ilike&quot; style=&quot;width: 62.3%;&quot;&gt;ILIKE: 0.1747&lt;/div&gt;
        &lt;/div&gt;
        &lt;div class=&quot;improvement&quot;&gt;BM25 leads by +60.6%&lt;/div&gt;
    &lt;/div&gt;

    &lt;div class=&quot;metric-group&quot;&gt;
        &lt;div class=&quot;metric-label&quot;&gt;Recall % (found relevant documents)&lt;/div&gt;
        &lt;div class=&quot;bars&quot;&gt;
            &lt;div class=&quot;bar bm25&quot; style=&quot;width: 100%;&quot;&gt;BM25: 66.7%&lt;/div&gt;
        &lt;/div&gt;
        &lt;div class=&quot;bars&quot;&gt;
            &lt;div class=&quot;bar ilike&quot; style=&quot;width: 42.1%;&quot;&gt;ILIKE: 28.1%&lt;/div&gt;
        &lt;/div&gt;
        &lt;div class=&quot;improvement&quot;&gt;BM25 leads by +38.6pp&lt;/div&gt;
    &lt;/div&gt;
&lt;/div&gt;

&lt;p&gt;The result is a fast, efficient, and typo-tolerant search engine built without any external dependencies. It was a fascinating journey into the mechanics of search, and it&apos;s incredibly satisfying to see it working so well. Hopefully, this sheds some light on how modern keyword search works and inspires you to see what you can build with the tools you already have.&lt;/p&gt;
</description>
                <pubDate>Sat, 16 Aug 2025 01:00:00 +0000</pubDate>
                <link>http://toranbillups.com/blog/archive/2025/08/16/building-keyword-search-in-postgres/</link>
                <guid isPermaLink="true">http://toranbillups.com/blog/archive/2025/08/16/building-keyword-search-in-postgres/</guid>
            </item>
        
            <item>
                <title>Writing the BERT Encoder with Nx</title>
                <description>&lt;p&gt;I set out to build a &lt;a href=&quot;https://github.com/toranb/encoder-search&quot;&gt;BERT like encoder&lt;/a&gt; from scratch this year. But that&apos;s not what happened. Instead I spent months stumbling over my inexperience, hitting walls, abandoning assumptions, and ultimately leaning on pretrained weights just to get off the ground. This is that retrospective—every wrong turn, every silent bug, and the hard-won lessons that came from building something I barely understood when I started.&lt;/p&gt;

&lt;h2&gt;The Vision and the Naivety&lt;/h2&gt;

&lt;p&gt;My original goal was ambitious: write an encoder from the ground up, train it on biblical text using Masked Language Modeling, and build a hybrid search for the New Testament. I wanted to understand embeddings at the deepest level, and this project was the proving ground. With the &lt;a href=&quot;https://arxiv.org/abs/1810.04805&quot;&gt;original BERT paper&lt;/a&gt; in hand I set out to reproduce their success on a much smaller corpus—and I was wildly underestimating what lay ahead. Nothing about this would come together quickly, and early wins like moving from SGD to the &lt;a href=&quot;https://docs.pytorch.org/docs/stable/generated/torch.optim.Adam.html&quot;&gt;Adam optimizer&lt;/a&gt; only revealed the volume of problems hiding underneath.&lt;/p&gt;

&lt;p&gt;What followed was months of exploration, discovery, dead ends, and hard-won lessons. Like all of my adventures, the mistakes were the most interesting part of the story.&lt;/p&gt;

&lt;h2&gt;The First Wall: Padding Masks&lt;/h2&gt;

&lt;p&gt;The biggest trap I fell into early on was assuming the compiler would find bugs for me. I jumped straight into training runs with complex architectures, convinced that more layers meant better results. I should have started by trying to memorize 100 tokens over 10 epochs to validate simple base assumptions.&lt;/p&gt;

&lt;p&gt;For a while I did see training loss improve, but validation loss was inconsistent. I didn&apos;t train long enough to see the plateau, which gave me a false impression that my attention implementation was working. Eventually I found that learning a single verse was possible, but when I tried to learn 10 verses—with an overfitting, repetitive dataset just to prove it could work—everything fell apart.&lt;/p&gt;

&lt;p&gt;That&apos;s when I pulled back to inspect the math behind each component individually.&lt;/p&gt;

&lt;p&gt;I used IO.inspect to print out the vectors for a shorter verse. The last 20 tokens were clearly padding IDs. I traced query, key, and value projections and realized I wasn&apos;t excluding padding positions from attention.&lt;/p&gt;

&lt;p&gt;The model was learning from padding. That single masking error was hiding behind what looked like architecture problems and sent me down more than a few dead ends.&lt;/p&gt;

&lt;p&gt;The fix was an additive mask that sets padding positions to negative infinity before softmax, effectively zeroing them out:&lt;/p&gt;

&lt;div class=&quot;highlight&quot; data-language=&quot;elixir&quot;&gt;
  &lt;pre class=&quot;language-elixir&quot;&gt;
    &lt;code class=&quot;language-elixir&quot;&gt;defn self_attention(input, w_query, w_key, w_value, w_out, attention_mask) do&lt;/code&gt;
    &lt;code class=&quot;language-elixir&quot;&gt;  # ... projection code ...&lt;/code&gt;
    &lt;code class=&quot;language-elixir&quot;&gt;&lt;/code&gt;
    &lt;code class=&quot;language-elixir&quot;&gt;  attention_scores = Nx.dot(q, [3], [0, 1], k, [3], [0, 1])&lt;/code&gt;
    &lt;code class=&quot;language-elixir&quot;&gt;  scaling_divisor = Nx.sqrt(head_dim)&lt;/code&gt;
    &lt;code class=&quot;language-elixir&quot;&gt;  scaled_attention_scores = Nx.divide(attention_scores, scaling_divisor)&lt;/code&gt;
    &lt;code class=&quot;language-elixir&quot;&gt;&lt;/code&gt;
    &lt;code class=&quot;language-elixir&quot;&gt;  # The fix: -1.0e8 makes padding positions effectively zero after softmax&lt;/code&gt;
    &lt;code class=&quot;language-elixir&quot;&gt;  additive_mask = Nx.select(attention_mask, 0.0, -1.0e8)&lt;/code&gt;
    &lt;code class=&quot;language-elixir&quot;&gt;  masked_scores = Nx.add(scaled_attention_scores, additive_mask)&lt;/code&gt;
    &lt;code class=&quot;language-elixir&quot;&gt;&lt;/code&gt;
    &lt;code class=&quot;language-elixir&quot;&gt;  attention_weights = softmax(masked_scores)&lt;/code&gt;
    &lt;code class=&quot;language-elixir&quot;&gt;  # ... rest of attention ...&lt;/code&gt;
    &lt;code class=&quot;language-elixir&quot;&gt;end&lt;/code&gt;
  &lt;/pre&gt;
&lt;/div&gt;

&lt;img src=&apos;/content/images/2025/masking.png&apos; alt=&apos;Attention mask matrix heatmap showing padding positions zeroed out with negative infinity&apos;/&gt;

&lt;p&gt;Once I properly implemented padding masks, performance jumped dramatically. The learning: there are no shortcuts—you need to verify your math step-by-step on a tiny dataset before scaling up.&lt;/p&gt;

&lt;h2&gt;The Second Wall: Gradient Chaos&lt;/h2&gt;

&lt;p&gt;For the longest time, I never computed or printed my gradients to evaluate them. I was hypnotized by my training and validation loss curves, completely ignoring the underlying mathematics.&lt;/p&gt;

&lt;p&gt;The symptom was that validation loss didn&apos;t have a nice curve. It would improve, then jump around, never settling into the steady descent I expected. So much code had been written at this point, and much of it wasn&apos;t properly validated.&lt;/p&gt;

&lt;p&gt;I spent some time digging into the &lt;a href=&quot;https://github.com/elixir-nx/polaris&quot;&gt;Polaris&lt;/a&gt; library so I could compute gradients, and with that additional information I was finally able to see the problem: gradient explosion.&lt;/p&gt;

&lt;p&gt;Before epoch 6 or 8, the gradient norm would be something like 12. But as training continued I would see 18, then 20, then 33—and it never came down. I consulted with Gemini about this pattern, and you might say I took the LLM&apos;s word as gospel: this was a bad signal indicating erratic learning.&lt;/p&gt;

&lt;p&gt;From here, I read about gradient clipping and discovered how the original BERT team clipped at 1.0. The fix was straightforward with Polaris:&lt;/p&gt;

&lt;div class=&quot;highlight&quot; data-language=&quot;elixir&quot;&gt;
  &lt;pre class=&quot;language-elixir&quot;&gt;
    &lt;code class=&quot;language-elixir&quot;&gt;{init_fn, update_fn} =&lt;/code&gt;
    &lt;code class=&quot;language-elixir&quot;&gt;  Polaris.Updates.clip_by_global_norm(max_norm: 1.0)&lt;/code&gt;
    &lt;code class=&quot;language-elixir&quot;&gt;  |&gt; Polaris.Updates.scale_by_adam()&lt;/code&gt;
    &lt;code class=&quot;language-elixir&quot;&gt;  |&gt; Polaris.Updates.add_decayed_weights(decay: 0.01)&lt;/code&gt;
    &lt;code class=&quot;language-elixir&quot;&gt;  |&gt; Polaris.Updates.scale_by_schedule(schedule_fn)&lt;/code&gt;
    &lt;code class=&quot;language-elixir&quot;&gt;&lt;/code&gt;
    &lt;code class=&quot;language-elixir&quot;&gt;init_optimizer_state = init_fn.(initial_params)&lt;/code&gt;
  &lt;/pre&gt;
&lt;/div&gt;

&lt;p&gt;This single change transformed my erratic training into steady, predictable progress. The learning: your gradients are telling you a story. Listen to them.&lt;/p&gt;

&lt;h2&gt;The Third Wall: Memory Leaks&lt;/h2&gt;

&lt;p&gt;As I started to scale up my encoder, my RTX 4090 ran out of vRAM by epoch 8 or 10. I moved to the cloud with &lt;a href=&quot;https://www.runpod.io/&quot;&gt;RunPod&lt;/a&gt; using my &lt;a href=&quot;https://github.com/toranb/runpod-cuda12&quot;&gt;runpod-cuda12 setup&lt;/a&gt;, and even an 80GB H100 failed after 16 hours. That was the clue: this wasn&apos;t a hardware limit—it was a leak.&lt;/p&gt;

&lt;p&gt;The issue was simple. I had Nx.backend_deallocate calls, but not in the innermost loop. The first batch of each epoch accumulated without release. Memory grew every epoch until the run collapsed around epoch 8-10.&lt;/p&gt;

&lt;p&gt;After moving deallocation into the inner loop, I was able to cut my cloud spending and go back to training at home with just one more trick: I cracked open the Nx types.ex file in my deps directory and changed the default float from f32 to bf16. This is still a direct hack—as of this writing, the configuration doesn&apos;t yet exist in Nx—but it allowed me to run without compromise in terms of layers, dimensionality, or attention heads.&lt;/p&gt;

&lt;h2&gt;The Architecture Shift: Pre-Layer Normalization&lt;/h2&gt;

&lt;p&gt;While training loss was improving, validation loss wasn&apos;t moving with my bigger dataset. After fixing the padding mask issue, I set my sights on this problem along with trying to dial in the number of layers and attention heads.&lt;/p&gt;

&lt;p&gt;I originally had post-layer normalization—the pattern from the original BERT paper where you normalize after the residual connection. After some chatting with Claude about my architecture, it recommended I try pre-layer normalization instead.&lt;/p&gt;

&lt;p&gt;The difference is subtle but important. Post-LN (original BERT):&lt;/p&gt;

&lt;div class=&quot;highlight&quot; data-language=&quot;elixir&quot;&gt;
  &lt;pre class=&quot;language-elixir&quot;&gt;
    &lt;code class=&quot;language-elixir&quot;&gt;# Post-LN: normalize AFTER residual add&lt;/code&gt;
    &lt;code class=&quot;language-elixir&quot;&gt;attn_out = self_attention(x, ...)&lt;/code&gt;
    &lt;code class=&quot;language-elixir&quot;&gt;add = residual_connection(x, attn_out)&lt;/code&gt;
    &lt;code class=&quot;language-elixir&quot;&gt;norm = layer_norm(add)&lt;/code&gt;
  &lt;/pre&gt;
&lt;/div&gt;

&lt;p&gt;Pre-LN (what I switched to):&lt;/p&gt;

&lt;div class=&quot;highlight&quot; data-language=&quot;elixir&quot;&gt;
  &lt;pre class=&quot;language-elixir&quot;&gt;
    &lt;code class=&quot;language-elixir&quot;&gt;# Pre-LN: normalize BEFORE sublayer&lt;/code&gt;
    &lt;code class=&quot;language-elixir&quot;&gt;norm_attn = layer_norm(x)&lt;/code&gt;
    &lt;code class=&quot;language-elixir&quot;&gt;attn_out = self_attention(norm_attn, ...)&lt;/code&gt;
    &lt;code class=&quot;language-elixir&quot;&gt;add = residual_connection(x, attn_out)&lt;/code&gt;
  &lt;/pre&gt;
&lt;/div&gt;

&lt;p&gt;Why does this matter? In post-LN, gradients must flow through normalization after the residual add. As you stack layers, 12 in my case, that compounding effect can either explode or vanish before it reaches early layers.&lt;/p&gt;

&lt;p&gt;Pre-LN keeps the residual path clean. The gradient can flow directly through x + sublayer_output without passing through normalization, while attention and FFN paths still get normalized.&lt;/p&gt;

&lt;img src=&apos;/content/images/2025/preln.png&apos; alt=&apos;Diagram comparing the gradient flow in Post-LN versus Pre-LN transformer blocks&apos;/&gt;

&lt;p&gt;The original BERT team used post-LN with 12 layers, but they also had massive compute, huge datasets, and the ability to tune endlessly. With fewer than 500,000 examples, I needed stability. Pre-LN gave me predictable gradients and a much more forgiving learning rate.&lt;/p&gt;

&lt;h2&gt;The Recipe Ceiling&lt;/h2&gt;

&lt;p&gt;After countless training runs and months of tuning the encoder, validation loss was stuck around 1.6. I&apos;d tried everything I could think of: adjusting dropout rates, tweaking learning rates, adding layers or attention head dimensions, increasing weight decay. Nothing broke through the floor. I was convinced I&apos;d hit a data ceiling—that 400,000 training examples simply weren&apos;t enough.&lt;/p&gt;

&lt;p&gt;Then I pulled in a fresh pair of eyes for a detailed code review, stepping through each component of the training loop. Within hours, two foundational bugs surfaced that had been compounding since my first training run.&lt;/p&gt;

&lt;p&gt;First, my dropout implementation was fundamentally wrong. I used a normal distribution for the mask, which quietly dropped far more activations than intended. The model was training under a much harsher regularizer than I thought, and the surviving activations were scaled as if nothing was wrong. It was a silent, destructive bug that made everything look like a data problem.&lt;/p&gt;

&lt;p&gt;The fix was textbook dropout—switch to a uniform distribution with a proper Bernoulli mask:&lt;/p&gt;

&lt;div class=&quot;highlight&quot; data-language=&quot;elixir&quot;&gt;
  &lt;pre class=&quot;language-elixir&quot;&gt;
    &lt;code class=&quot;language-elixir&quot;&gt;defn dropout(input, key, training) do&lt;/code&gt;
    &lt;code class=&quot;language-elixir&quot;&gt;  if training do&lt;/code&gt;
    &lt;code class=&quot;language-elixir&quot;&gt;    mask_shape = Nx.shape(input)&lt;/code&gt;
    &lt;code class=&quot;language-elixir&quot;&gt;    random_vals = Nx.Random.uniform_split(key, 0.0, 1.0, shape: mask_shape)&lt;/code&gt;
    &lt;code class=&quot;language-elixir&quot;&gt;    keep_prob = Nx.subtract(1.0, @dropout_rate)&lt;/code&gt;
    &lt;code class=&quot;language-elixir&quot;&gt;    keep_mask = Nx.less(random_vals, keep_prob)&lt;/code&gt;
    &lt;code class=&quot;language-elixir&quot;&gt;    scale_factor = Nx.divide(1.0, keep_prob)&lt;/code&gt;
    &lt;code class=&quot;language-elixir&quot;&gt;    input |&gt; Nx.multiply(keep_mask) |&gt; Nx.multiply(scale_factor)&lt;/code&gt;
    &lt;code class=&quot;language-elixir&quot;&gt;  else&lt;/code&gt;
    &lt;code class=&quot;language-elixir&quot;&gt;    input&lt;/code&gt;
    &lt;code class=&quot;language-elixir&quot;&gt;  end&lt;/code&gt;
    &lt;code class=&quot;language-elixir&quot;&gt;end&lt;/code&gt;
  &lt;/pre&gt;
&lt;/div&gt;

&lt;p&gt;Second, my masked language modeling targets were static. Masking happened once during preprocessing, and every epoch saw the exact same masked positions with the same targets. The model could memorize &quot;token 47 is always masked in example 312&quot; rather than learning general language structure. With static masks, my 400,000 examples were effectively a much smaller dataset because the supervised signal never varied.&lt;/p&gt;

&lt;p&gt;The fix was to separate tokenization from masking—tokenize once during preprocessing, then apply masks dynamically per batch at training time. This mirrors standard BERT practice and means the model sees different masked positions every epoch, turning each example into many variants of itself.&lt;/p&gt;

&lt;p&gt;The results were immediate and dramatic:&lt;/p&gt;

&lt;img src=&apos;/content/images/2025/curves.png&apos; alt=&apos;Loss curves before and after fixing dropout and dynamic masking&apos;/&gt;

&lt;p&gt;By epoch 8, Run 8 already beat the best validation loss across all seven previous runs. By epoch 12, it reached 1.124—a 29% improvement over a ceiling I&apos;d spent months trying to break through. The overfitting I&apos;d been fighting wasn&apos;t a data ceiling at all. It was a recipe ceiling.&lt;/p&gt;

&lt;h2&gt;The Missing Projection: Weight Tying&lt;/h2&gt;

&lt;p&gt;With my loss curves looking better than ever, I thought I was finally at the end of my journey. Unfortunately, my evals showed a lack of depth because of another critical detail I skipped over early on.&lt;/p&gt;

&lt;p&gt;At some point I came to grips with the reality that I couldn&apos;t train weights from scratch like the team behind BERT—I didn&apos;t have a dataset anywhere near 3 billion tokens. So instead I gave myself a jump start by borrowing the initial weights from all 12 layers of BERT-base. The trouble was that I forgot to include the vocab projection weights, so the final MLM head failed to project my learned geometry into the proper vocabulary.&lt;/p&gt;

&lt;p&gt;My evaluation suite told the whole story. The encoder clearly understood theology—75% accuracy on contrastive tests—but couldn&apos;t predict the right vocabulary words at masked positions:&lt;/p&gt;

&lt;div class=&quot;highlight&quot; data-language=&quot;elixir&quot;&gt;
  &lt;pre class=&quot;language-elixir&quot;&gt;
    &lt;code class=&quot;language-elixir&quot;&gt;DOC_001: expected=grace    got=[your(0.37), their(0.20), per(0.10), rep(0.05), ab(0.03)]&lt;/code&gt;
    &lt;code class=&quot;language-elixir&quot;&gt;DOC_010: expected=image    got=[the(0.98), and(0.01), ##the(0.00), ##d(0.00), ##th(0.00)]&lt;/code&gt;
    &lt;code class=&quot;language-elixir&quot;&gt;DOC_025: expected=kingdom  got=[father(0.999), ##otte(0.00), beg(0.00), ##k(0.00), ab(0.00)]&lt;/code&gt;
  &lt;/pre&gt;
&lt;/div&gt;

&lt;p&gt;The model was predicting function words, subword fragments, and punctuation instead of theological terms. Even at k=50, not a single doctrinal target appeared. The encoder&apos;s internal representations were strong, but the output projection to vocabulary was completely broken because I had initialized my MLM output weights randomly and never tied them to the embedding matrix.&lt;/p&gt;

&lt;p&gt;In a BERT encoder, two matrices deal with the vocabulary. The embedding matrix maps token IDs to 768-dimensional vectors at the input. The output projection maps those 768-dimensional hidden states back to vocabulary logits at masked positions. These two matrices perform inverse operations—one goes from vocabulary to hidden space, the other goes back. Weight tying makes this relationship explicit by using the same matrix for both:&lt;/p&gt;

&lt;div class=&quot;highlight&quot; data-language=&quot;elixir&quot;&gt;
  &lt;pre class=&quot;language-elixir&quot;&gt;
    &lt;code class=&quot;language-elixir&quot;&gt;# Without weight tying: two independent matrices&lt;/code&gt;
    &lt;code class=&quot;language-elixir&quot;&gt;logits = Nx.dot(hidden_states, out_w) |&gt; Nx.add(out_b)&lt;/code&gt;
    &lt;code class=&quot;language-elixir&quot;&gt;&lt;/code&gt;
    &lt;code class=&quot;language-elixir&quot;&gt;# With weight tying: one matrix, transposed for output&lt;/code&gt;
    &lt;code class=&quot;language-elixir&quot;&gt;logits = Nx.dot(hidden_states, Nx.transpose(embeddings)) |&gt; Nx.add(out_b)&lt;/code&gt;
  &lt;/pre&gt;
&lt;/div&gt;

&lt;p&gt;The out_w parameter disappears entirely. The embedding matrix serves double duty, and every gradient that updates the embedding for &quot;grace&quot; simultaneously updates how the model predicts &quot;grace&quot; at masked positions. Without weight tying, I was asking a random matrix to decode a learned representation space—hoping that 768 x 30,522 parameters would independently converge to match the encoder&apos;s geometry. They never did.&lt;/p&gt;

&lt;p&gt;This also cut ~23 million trainable parameters, which meant less memory and smaller optimizer state. More importantly, it eliminated a failure mode: if the encoder knows what &quot;grace&quot; means, it could now predict &quot;grace&quot; at a masked position, because the same vector represents both.&lt;/p&gt;

&lt;h2&gt;Aligning for Search: Contrastive Training&lt;/h2&gt;

&lt;p&gt;With a solid MLM training behind me, I set my sights on the work ahead to align this encoder for search tasks. MLM taught the model to understand tokens in context—&quot;fill in the blank&quot;—but search requires something different. I needed similar meanings to cluster together in the embedding space so that a query and a relevant verse would land near each other.&lt;/p&gt;

&lt;p&gt;To get there, I put together a dataset of paired verses drawn from different Bible translations. The same verse in the NLT and NIV is a natural positive pair—same meaning, different wording. I then took the final MLM weights and trained with a contrastive loss function that would pull matching pairs closer together while pushing everything else apart.&lt;/p&gt;

&lt;p&gt;The approach works by encoding both sides of a pair through the same encoder, mean-pooling the token outputs into a single sentence vector (with pad masking so padding doesn&apos;t count), and then L2-normalizing so cosine similarity becomes a simple dot product. For a batch of 64 pairs, this builds a 64x64 similarity matrix where the diagonal entries are the correct matches and everything off-diagonal is an implicit negative:&lt;/p&gt;

&lt;p&gt;While this was the simplest post-training and least error prone step throughout, it still came with some valuable learnings. The one that stuck with me is the key difference in the loss functions. In this training run the model is no longer predicting individual tokens. It&apos;s answering: &quot;which sentence out of these 64 (batch size) is the true partner?&quot;&lt;/p&gt;

&lt;p&gt;This fine-tuning step took the encoder from understanding words in context to understanding that two differently worded verses about the same concept should live in the same neighborhood of embedding space.&lt;/p&gt;

&lt;h2&gt;The Last Mile: Query-to-Verse Alignment&lt;/h2&gt;

&lt;p&gt;After contrastive training, I had an encoder that grouped similar verses together but there was still a gap between how the encoder organized scripture and how a person actually searches. People usually search with short phrases or even questions shaped by their own vocabulary and experience with other software, which often doesn&apos;t overlap with the biblical text at all.&lt;/p&gt;

&lt;p&gt;To close that gap, I generated a dataset of 1,400 high-quality query-to-verse mappings. Each pair linked a natural search phrase to the verse it was really asking about. For example, &quot;assurance of salvation&quot; mapped to Romans 8:1—&quot;there is therefore now no condemnation for those who are in Christ Jesus.&quot; The verse never mentions the word &quot;salvation,&quot; but it speaks directly to that theology.&lt;/p&gt;

&lt;p&gt;The training used the same contrastive loss as before, but with a fundamentally different dataset. Instead of teaching the encoder that two translations of the same verse should be neighbors, I was teaching it that a human search query and its best answer should be neighbors. This bent the geometry one final time —pulling the embedding space toward how people actually look for scripture, not just how scripture relates to itself.&lt;/p&gt;

&lt;p&gt;This was the smallest dataset of the three training phases, but arguably the most targeted. The 1,400 pairs acted as precise instructions: when someone asks about &quot;forgiveness after failure,&quot; the encoder should point toward the prodigal son, not just verses that happen to contain the word &quot;forgiveness.&quot; Quality mattered far more than quantity here, requiring loads of synthetic data engineering and genuine theological care to validate the mappings before training.&lt;/p&gt;

&lt;h2&gt;The Missing Baseline: BM25&lt;/h2&gt;

&lt;p&gt;If I had to do it all over again, I would have started with &lt;a href=&quot;/blog/archive/2025/08/16/building-keyword-search-in-postgres/&quot;&gt;BM25&lt;/a&gt; as my baseline from day one—not bolted it on toward the end as an afterthought. Keyword search isn&apos;t just a fallback. It&apos;s a genuinely powerful complement to semantic search, and understanding why made the final hybrid experience far better than either approach alone.&lt;/p&gt;

&lt;p&gt;BM25 and semantic search fail in complementary ways. BM25 excels when the user&apos;s words overlap with the text—exact phrases, proper nouns, specific references. The encoder excels when they don&apos;t: thematic queries like &quot;assurance of salvation&quot; or &quot;fruit of the spirit&quot; surface verses that speak to the concept without containing the exact keywords. The keyword signal anchors results for literal queries while the encoder lifts thematic results that pure text matching would miss.&lt;/p&gt;

&lt;p&gt;The challenge with combining them is that their scores live on completely different scales, so a naive weighted sum is notoriously difficult to tune. Reciprocal Rank Fusion sidesteps the problem entirely by ignoring raw scores and working only with rank positions—a document ranked highly by both rises to the top without any normalization at all.&lt;/p&gt;

&lt;p&gt;Starting with BM25 as the baseline would have given me a strong search experience much earlier and a clearer benchmark for measuring what the encoder actually added.&lt;/p&gt;

&lt;h2&gt;The Data Reckoning&lt;/h2&gt;

&lt;p&gt;More than anything, I spent months building, curating, and tuning my dataset. I knew I couldn&apos;t achieve the same size and diversity the original BERT team had, but I was pleasantly surprised with what I could accomplish with just under 500,000 unique training examples.&lt;/p&gt;

&lt;p&gt;The trouble was I made several mistakes that cost me significant time:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;False diversity.&lt;/strong&gt; I wanted a diverse set of Bible text and found myself using different translations—NLT, NIV, ESV, NET. This seemed like a good idea, but I didn&apos;t actually look at the dataset closely. While subtle differences exist between translations, they share similar tokens and themes. They aren&apos;t radically different. I later learned I should have included genuinely different text like Mere Christianity, rich theological writing that isn&apos;t the original Bible text.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Quote noise.&lt;/strong&gt; Biblical text has a lot of quotation marks, and I found this was distracting to the masked token prediction with my simplistic encoder. The model was learning patterns around quote boundaries rather than semantic meaning.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Lazy first pass.&lt;/strong&gt; While I expanded to books over time, I didn&apos;t do my best work on the first pass. A lot of that text data was less than ideal until I circled back to do real cleaning—which only happened by actually reading the text in those contextualized chunks.&lt;/p&gt;

&lt;p&gt;If I had to synthesize what I would do differently, it comes down to this: look at the data more. I underestimated the time involved in data work, which has become a recurring theme throughout my machine learning journey.&lt;/p&gt;

&lt;h2&gt;The Invisible Bugs&lt;/h2&gt;

&lt;p&gt;Looking back across every wall I hit, the pattern was always the same: something that appeared to work was silently wrong.&lt;/p&gt;

&lt;p&gt;Padding masks didn&apos;t throw errors—attention just quietly attended to nonsense. Dropout didn&apos;t crash—it just dropped 56% of activations instead of 16%. Static masking didn&apos;t fail—it let the model memorize targets instead of learning language. The output projection didn&apos;t break—it just never converged to match the encoder&apos;s geometry. In every case, training loss went down and nothing in the logs said something was broken.&lt;/p&gt;

&lt;p&gt;The most dangerous bugs in machine learning are the ones that look like they&apos;re working. If you&apos;re building a model from scratch, the single most valuable habit I can offer is this: validate every component in isolation on a tiny dataset before scaling up. Print the vectors. Inspect the gradients. Check that your dropout rate actually drops what you think it drops. And when you hit what looks like a ceiling, get a fresh pair of eyes on your code before you assume it&apos;s a data problem.&lt;/p&gt;

&lt;p&gt;You can find the source code on &lt;a href=&quot;https://github.com/toranb/encoder-search&quot;&gt;github&lt;/a&gt;.&lt;/p&gt;
</description>
                <pubDate>Sun, 22 Jun 2025 01:00:00 +0000</pubDate>
                <link>http://toranbillups.com/blog/archive/2025/06/22/bert-from-scratch-with-nx/</link>
                <guid isPermaLink="true">http://toranbillups.com/blog/archive/2025/06/22/bert-from-scratch-with-nx/</guid>
            </item>
        
            <item>
                <title>Adventures with Synthetic Data</title>
                <description>&lt;p&gt;After &lt;a href=&quot;https://www.youtube.com/watch?v=-iZIZHgHa5M&quot;&gt;ElixirConf&lt;/a&gt; I found myself increasingly immersed in the world of fine-tuning. This curiosity soon led me on a journey to explore various use cases, eventually drawing me into the realm of synthetic data.&lt;/p&gt;

&lt;p&gt;One pivotal moment in this exploration was &lt;a href=&quot;https://edwarddonner.com/2024/01/11/fine-tune-llama-for-text-messages-part-1&quot;&gt;a post by Edward Donner&lt;/a&gt; that detailed his experience training a language model to mimic his unique texting style. The majority of my time was soon dedicated to cleaning raw text message data in order to create a high-quality dataset for fine-tuning.&lt;/p&gt;

&lt;p&gt;While &lt;a href=&quot;https://www.youtube.com/watch?v=R0VJIW0IYPo&quot;&gt;the talk&lt;/a&gt; was focused on synthetic data generation, I also delved into the intricacies of fine-tuning with &lt;a href=&quot;https://github.com/unslothai/unsloth&quot;&gt;Unsloth&lt;/a&gt;, evaluation strategies, and even model deployment and serving with &lt;a href=&quot;&quot;https://github.com/elixir-nx/nx&gt;Nx&lt;/a&gt;. I owe much of the data generation techniques I shared to &lt;a href=&quot;https://x.com/jon_durbin&quot;&gt;Jon Durbin&lt;/a&gt;, whose DPO approach in particular proved instrumental to my work. Lastly, a big thank you to &lt;a href=&quot;https://paraxial.io/&quot;&gt;Paraxial IO&lt;/a&gt; for giving me the opportunity to share my insights!&lt;/p&gt;

&lt;div style=&quot;padding-top: 20px; padding-bottom: 20px;&quot;&gt;
  &lt;div style=&quot;margin: auto; width: 90vw; height: 50vw; max-width: 768px; max-height: 432px&quot;&gt;
    &lt;iframe width=&quot;100%&quot; height=&quot;100%&quot; src=&quot;https://www.youtube.com/embed/R0VJIW0IYPo?si=_DC60mccDxzhyIHA&quot; frameborder=&quot;0&quot; allow=&quot;accelerometer; autoplay; encrypted-media; gyroscope; picture-in-picture&quot; allowfullscreen&gt;&lt;/iframe&gt;
  &lt;/div&gt;
&lt;/div&gt;

&lt;p&gt;The python source code for fine tuning with unsloth is on &lt;a href=&quot;https://github.com/toranb/sloth&quot;&gt;Github&lt;/a&gt; for anyone interested. I also put the elixir code for the &lt;a href=&quot;https://github.com/toranb/mistral-chat-f16&quot;&gt;chat app&lt;/a&gt; shown in the talk on Github.&lt;/p&gt;
</description>
                <pubDate>Tue, 07 May 2024 01:00:00 +0000</pubDate>
                <link>http://toranbillups.com/blog/archive/2024/05/07/adventures-with-synthetic-data/</link>
                <guid isPermaLink="true">http://toranbillups.com/blog/archive/2024/05/07/adventures-with-synthetic-data/</guid>
            </item>
        
            <item>
                <title>Retrieval Augmented Generation Cohort</title>
                <description>&lt;p&gt;On November 1st I joined the first &lt;a href=&quot;https://blogs.nvidia.com/blog/what-is-retrieval-augmented-generation/&quot;&gt;RAG&lt;/a&gt; cohort from &lt;a href=&quot;https://mlops.community/&quot;&gt;MLOps.Community&lt;/a&gt; and went hard for 8 weeks building a proof of concept that combined everything from &lt;a href=&quot;https://huggingface.co/blog/getting-started-with-embeddings&quot;&gt;embedding models&lt;/a&gt;, &lt;a href=&quot;https://developers.google.com/machine-learning/resources/prompt-eng&quot;&gt;prompting&lt;/a&gt;, and retrieval to &lt;a href=&quot;https://qdrant.tech/articles/hybrid-search/#the-precise-search-the-re-ranking-strategy&quot;&gt;ranking&lt;/a&gt; with a cross encoder to support &lt;a href=&quot;https://github.com/pgvector/pgvector-python/blob/master/examples/hybrid_search.py#L37-L59&quot;&gt;hybrid search&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;I want to extend a public &quot;thank you&quot; to &lt;a href=&quot;https://twitter.com/RahulParundekar&quot;&gt;Rahul Parundekar&lt;/a&gt; for all his hard work ahead of the long journey, the consistent encouragement and the 1v1 coaching! If anyone needs a machine learning engineer and coach, don&apos;t hesitate to get in touch with Rahul.&lt;/p&gt;

&lt;p&gt;I put together a succinct version of the &lt;a href=&quot;https://vimeo.com/900118849&quot;&gt;final presentation&lt;/a&gt; to document my learning, commitments, growth and ultimately to keep me accountable. The entire experience simultaneously energized and propelled me further down the road of machine learning!&lt;/p&gt;

&lt;div style=&quot;padding-top: 20px; padding-bottom: 20px;&quot;&gt;
  &lt;div style=&quot;margin: auto; width: 90vw; height: 50vw; max-width: 768px; max-height: 432px&quot;&gt;
    &lt;iframe src=&quot;https://player.vimeo.com/video/900118849&quot; style=&quot;width:100%;height:100%;&quot; frameborder=&quot;0&quot; allow=&quot;autoplay; fullscreen; picture-in-picture&quot; allowfullscreen&gt;&lt;/iframe&gt;
  &lt;/div&gt;
  &lt;script src=&quot;https://player.vimeo.com/api/player.js&quot;&gt;&lt;/script&gt;
&lt;/div&gt;

&lt;p&gt;The source code is up on &lt;a href=&quot;https://github.com/toranb/rag-n-drop&quot;&gt;Github&lt;/a&gt; for anyone who wanted to see the final example with hybrid search.&lt;/p&gt;
</description>
                <pubDate>Sat, 06 Jan 2024 01:00:00 +0000</pubDate>
                <link>http://toranbillups.com/blog/archive/2024/01/06/retrieval-augmented-generation-cohort/</link>
                <guid isPermaLink="true">http://toranbillups.com/blog/archive/2024/01/06/retrieval-augmented-generation-cohort/</guid>
            </item>
        
            <item>
                <title>Fine tuning language models with Axon</title>
                <description>&lt;p&gt;In 2023 I spent a lot of time at the intersection of software engineering and machine learning hoping to uncover the next great opportunity for automation. &lt;a href=&quot;https://www.youtube.com/watch?v=-iZIZHgHa5M&quot;&gt;My talk&lt;/a&gt; at &lt;a href=&quot;https://2023.elixirconf.com/&quot;&gt;ElixirConf US&lt;/a&gt; documents my nearly year-long effort to deconstruct deep learning for those who feel overwhelmed by the technical aspects.&lt;/p&gt;

&lt;p&gt;I started with &lt;a href=&quot;https://blog.codinghorror.com/fizzbuzz-the-programmers-stairway-to-heaven/&quot;&gt;FizzBuzz&lt;/a&gt; because it was a problem most software engineers had some familiarity with which allowed me to subtly bridge the gap to more complex concepts. This widely known interview question was the perfect primer on the subject because underneath the diverse solutions was a well-understood classification problem that provided the optimal foundation for machine learning.&lt;/p&gt;

&lt;p&gt;After the more introductory concepts I discussed fine-tuning pre-trained models for text classification. I went into detail about creating a labeled dataset, tokenization, transforming text into embeddings and finally, fine tuning the model with Axon. I emphasized the critical role of data quality, the challenges that come with preparing or cleaning data, and the nuances of encoding text for machine learning models.&lt;/p&gt;

&lt;p&gt;Toward the end of the talk I spoke about the trend towards off-the-shelf, open source models, and the opportunities for developers to upskill. For those eager to delve deeper, I recommend &lt;a href=&quot;https://www.manning.com/books/grokking-deep-learning&quot;&gt;Grokking Deep Learning&lt;/a&gt; as an invaluable resource. This book greatly simplified the topic and inspired me to learn about neural networks and share with others.&lt;/p&gt;

&lt;p&gt;My goal was to make deep learning and model fine-tuning more approachable, especially for those in the Elixir community. While this talk was presented at ElixirConf I actually think it&apos;s applicable to all who consider themselves unfamiliar with the basics of machine learning.&lt;/p&gt;

&lt;p&gt;Looking back, this journey has been about more than just acquiring knowledge; it&apos;s about sharing it, making the path less intimidating, and most importantly, showing how software engineering and machine learning collide to make way for innovation.&lt;/p&gt;

&lt;div style=&quot;padding-top: 20px; padding-bottom: 20px;&quot;&gt;
  &lt;div style=&quot;margin: auto; width: 90vw; height: 50vw; max-width: 768px; max-height: 432px&quot;&gt;
    &lt;iframe width=&quot;100%&quot; height=&quot;100%&quot; src=&quot;https://www.youtube.com/embed/-iZIZHgHa5M?si=QUUNTUtgAVDdfPfz&quot; title=&quot;YouTube video player&quot; frameborder=&quot;0&quot; allow=&quot;accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share&quot; allowfullscreen&gt;&lt;/iframe&gt;
  &lt;/div&gt;
&lt;/div&gt;

&lt;p&gt;The source code is up on &lt;a href=&quot;https://github.com/toranb/elixirconf2023&quot;&gt;Github&lt;/a&gt; for anyone who wanted to see a few of those examples in more detail. The full transcript for the talk can be downloaded &lt;a href=&quot;/content/presentations/2024/finetune.txt&quot;&gt;here&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;I did share a complete FizzBuzz implementation I put together with &lt;a href=&quot;https://gist.github.com/toranb/56a3e65ca81fba1c4f6c92c6f1857681&quot;&gt;Nx&lt;/a&gt; for those who want to see the most basic numerical computing solution in Elixir. The &lt;a href=&quot;https://gist.github.com/toranb/e5c48565e83e4baaaf2c5850531a8a58&quot;&gt;Axon&lt;/a&gt; example shows a more declarative approach that ultimately solves the same problem.&lt;/p&gt;
</description>
                <pubDate>Thu, 02 Nov 2023 01:00:00 +0000</pubDate>
                <link>http://toranbillups.com/blog/archive/2023/11/02/fine-tuning-language-models-with-axon/</link>
                <guid isPermaLink="true">http://toranbillups.com/blog/archive/2023/11/02/fine-tuning-language-models-with-axon/</guid>
            </item>
        
            <item>
                <title>Fine tune Mistral 7B with the RTX 4090 and serve it with Nx</title>
                <description>&lt;p&gt;Fine-tuning with &lt;a href=&quot;https://hexdocs.pm/bumblebee/fine_tuning.html&quot;&gt;Bumblebee&lt;/a&gt; is great but large models such as &lt;a href=&quot;https://huggingface.co/mistralai/Mistral-7B-v0.1&quot;&gt;Mistral 7B&lt;/a&gt; demand over 100GB of vRAM to fine tune with full precision. To efficiently fine-tune this on a single RTX 4090 with only 24GB of vRAM, I turned to the open source Python project &lt;a href=&quot;https://github.com/Lightning-AI/lit-gpt&quot;&gt;lit-gpt&lt;/a&gt;. This approach enabled me to fine-tune locally, providing several advantages including fast feedback and the ability to keep proprietary data from external providers.&lt;/p&gt;

&lt;h3&gt;Setup&lt;/h3&gt;

&lt;p&gt;Although the process is well &lt;a href=&quot;https://github.com/Lightning-AI/lit-gpt/blob/main/tutorials/download_mistral.md&quot;&gt;documented&lt;/a&gt;, I decided to outline the steps required for myself just as much as anyone else.&lt;/p&gt;

&lt;div class=&quot;highlight&quot; data-language=&quot;elixir&quot;&gt;
  &lt;pre class=&quot;language-elixir&quot;&gt;
    &lt;code class=&quot;language-elixir&quot;&gt;$ git clone https://github.com/Lightning-AI/lit-gpt lit&lt;/code&gt;
    &lt;code class=&quot;language-elixir&quot;&gt;$ cd lit&lt;/code&gt;
    &lt;code class=&quot;language-elixir&quot;&gt;$ git checkout bf60124fa72a56436c7d4fecc093c7fc48e84433&lt;/code&gt;
    &lt;code class=&quot;language-elixir&quot;&gt;$ pip install -r requirements.txt&lt;/code&gt;
    &lt;code class=&quot;language-elixir&quot;&gt;$ python3 scripts/download.py --repo_id mistralai/Mistral-7B-v0.1&lt;/code&gt;
    &lt;code class=&quot;language-elixir&quot;&gt;$ python3 scripts/convert_hf_checkpoint.py --checkpoint_dir checkpoints/mistralai/Mistral-7B-v0.1&lt;/code&gt;
  &lt;/pre&gt;
&lt;/div&gt;

&lt;h3&gt;Data engineering&lt;/h3&gt;

&lt;p&gt;Next we need a dataset to fine tune the model with. Unlike the &lt;a href=&quot;https://toranbillups.com/blog/archive/2023/10/15/fine-tune-llama-2-and-serve-with-nx/&quot;&gt;llama 2 example&lt;/a&gt; where I fine tuned for dialog I instead wanted to fine tune for capability with Mistral 7B to see what the model was capable of learning. I &lt;a href=&quot;https://huggingface.co/jondurbin/airoboros-m-7b-3.1.2&quot;&gt;found a great fine tuned model&lt;/a&gt; worth emulating that creates expressions in JSON that &lt;a href=&quot;https://pypi.org/project/mathjson-solver/&quot;&gt;mathjson_solver&lt;/a&gt; can solve with. The dataset has questions and answers labeled with `instruction` and `output` respectively.&lt;/p&gt;

&lt;div class=&quot;highlight&quot; data-language=&quot;elixir&quot;&gt;
  &lt;pre class=&quot;language-elixir&quot;&gt;
    &lt;code class=&quot;language-elixir&quot;&gt;[&lt;/code&gt;
    &lt;code class=&quot;language-elixir&quot;&gt;  {&lt;/code&gt;
    &lt;code class=&quot;language-elixir&quot;&gt;    &quot;input&quot;: &quot;&quot;,&lt;/code&gt;
    &lt;code class=&quot;language-elixir&quot;&gt;    &quot;instruction&quot;: &quot;Create a MathJSON solution to the following:\nPhillip is taking a math test and an English test on Monday. The math test has 40 questions and he gets 75% of them right. The English test has 50 questions and he gets 98% of them right. How many total questions does he get right?&quot;,&lt;/code&gt;
    &lt;code class=&quot;language-elixir&quot;&gt;    &quot;output&quot;: &quot;&lt;mathjson&gt;\n[\n  \&quot;Add\&quot;,\n  [\n    \&quot;Multiply\&quot;,\n    40,\n    0.75\n  ],\n  [\n    \&quot;Multiply\&quot;,\n    50,\n    0.98\n  ]\n]\n&lt;/mathjson&gt;&quot;&lt;/code&gt;
    &lt;code class=&quot;language-elixir&quot;&gt;  }&lt;/code&gt;
    &lt;code class=&quot;language-elixir&quot;&gt;]&lt;/code&gt;
  &lt;/pre&gt;
&lt;/div&gt;

&lt;p&gt;With the instruction JSON we simply copy that file into the directory and run a script to prepare the dataset for fine tuning.&lt;/p&gt;

&lt;div class=&quot;highlight&quot; data-language=&quot;elixir&quot;&gt;
  &lt;pre class=&quot;language-elixir&quot;&gt;
    &lt;code class=&quot;language-elixir&quot;&gt;$ mkdir -p data/alpaca&lt;/code&gt;
    &lt;code class=&quot;language-elixir&quot;&gt;$ cd data/alpaca&lt;/code&gt;
    &lt;code class=&quot;language-elixir&quot;&gt;$ cp ~/somefolder/demo.json .&lt;/code&gt;
    &lt;code class=&quot;language-elixir&quot;&gt;$ cd ../../&lt;/code&gt;
    &lt;code class=&quot;language-elixir&quot;&gt;$ python3 scripts/prepare_alpaca.py --checkpoint_dir checkpoints/mistralai/Mistral-7B-v0.1 --data_file_name demo.json&lt;/code&gt;
  &lt;/pre&gt;
&lt;/div&gt;

&lt;h3&gt;Fine tune Mistral 7B&lt;/h3&gt;

&lt;p&gt;Once the data is split into test and training sets we are finally ready to fine tune Mistral 7B. It&apos;s worth mentioning that we are not fine tuning with full precision because we are tuning with a single &lt;a href=&quot;https://www.nvidia.com/en-us/geforce/graphics-cards/40-series/rtx-4090/&quot;&gt;RTX 4090 24GB&lt;/a&gt;.&lt;/p&gt;

&lt;div class=&quot;highlight&quot; data-language=&quot;elixir&quot;&gt;
  &lt;pre class=&quot;language-elixir&quot;&gt;
    &lt;code class=&quot;language-elixir&quot;&gt;$ python3 finetune/lora.py --data_dir data/alpaca --checkpoint_dir checkpoints/mistralai/Mistral-7B-v0.1 --precision bf16-true --quantize bnb.nf4&lt;/code&gt;
  &lt;/pre&gt;
&lt;/div&gt;

&lt;p&gt;After the fine tuning process we need to merge the weights.&lt;/p&gt;

&lt;div class=&quot;highlight&quot; data-language=&quot;elixir&quot;&gt;
  &lt;pre class=&quot;language-elixir&quot;&gt;
    &lt;code class=&quot;language-elixir&quot;&gt;$ mkdir -p out/lora_merged/Mistral-7B-v0.1&lt;/code&gt;
    &lt;code class=&quot;language-elixir&quot;&gt;$ python3 scripts/merge_lora.py --checkpoint_dir checkpoints/mistralai/Mistral-7B-v0.1 --lora_path out/lora/alpaca/lit_model_lora_finetuned.pth --out_dir out/lora_merged/Mistral-7B-v0.1&lt;/code&gt;
  &lt;/pre&gt;
&lt;/div&gt;

&lt;p&gt;To run this model we first need to copy over a few files from the original model.&lt;/p&gt;

&lt;div class=&quot;highlight&quot; data-language=&quot;elixir&quot;&gt;
  &lt;pre class=&quot;language-elixir&quot;&gt;
    &lt;code class=&quot;language-elixir&quot;&gt;$ cd out/lora_merged/Mistral-7B-v0.1&lt;/code&gt;
    &lt;code class=&quot;language-elixir&quot;&gt;$ cp ~/lit/checkpoints/mistralai/Mistral-7B-v0.1/tokenizer.model .&lt;/code&gt;
    &lt;code class=&quot;language-elixir&quot;&gt;$ cp ~/lit/checkpoints/mistralai/Mistral-7B-v0.1/*.json .&lt;/code&gt;
  &lt;/pre&gt;
&lt;/div&gt;

&lt;h3&gt;Evaluate&lt;/h3&gt;

&lt;p&gt;Before we serve the model with Nx it&apos;s important to evaluate it first. This is optional but it does offer a simple way to verify the model has learned something.&lt;/p&gt;

&lt;div class=&quot;highlight&quot; data-language=&quot;elixir&quot;&gt;
  &lt;pre class=&quot;language-elixir&quot;&gt;
    &lt;code class=&quot;language-elixir&quot;&gt;$ pip install sentencepiece&lt;/code&gt;
    &lt;code class=&quot;language-elixir&quot;&gt;$ python3 chat/base.py --checkpoint_dir out/lora_merged/Mistral-7B-v0.1&lt;/code&gt;
  &lt;/pre&gt;
&lt;/div&gt;

&lt;h3&gt;Serving with Nx&lt;/h3&gt;

&lt;p&gt;If the model is performing well enough we can pull over the 2 pytorch model bin files and copy the config file so Bumblebee can find it.&lt;/p&gt;

&lt;div class=&quot;highlight&quot; data-language=&quot;elixir&quot;&gt;
  &lt;pre class=&quot;language-elixir&quot;&gt;
    &lt;code class=&quot;language-elixir&quot;&gt;$ cd out/lora_merged/Mistral-7B-v0.1&lt;/code&gt;
    &lt;code class=&quot;language-elixir&quot;&gt;$ cp ~/lit/checkpoints/mistralai/Mistral-7B-v0.1/pytorch_model-00001-of-00002.bin .&lt;/code&gt;
    &lt;code class=&quot;language-elixir&quot;&gt;$ cp ~/lit/checkpoints/mistralai/Mistral-7B-v0.1/pytorch_model-00002-of-00002.bin .&lt;/code&gt;
    &lt;code class=&quot;language-elixir&quot;&gt;$ cp lit_config.json config.json&lt;/code&gt;
  &lt;/pre&gt;
&lt;/div&gt;

&lt;p&gt;To test this end to end we point Nx at the file system instead of pulling Mistral 7B from &lt;a href=&quot;https://huggingface.co&quot;&gt;hugging face&lt;/a&gt;.&lt;/p&gt;

&lt;div class=&quot;highlight&quot; data-language=&quot;elixir&quot;&gt;
  &lt;pre class=&quot;language-elixir&quot;&gt;
    &lt;code class=&quot;language-elixir&quot;&gt;def serving() do&lt;/code&gt;
    &lt;code class=&quot;language-elixir&quot;&gt;  mistral = {:local, &quot;/home/toranb/lit/out/lora_merged/Mistral-7B-v0.1&quot;}&lt;/code&gt;
    &lt;code class=&quot;language-elixir&quot;&gt;  {:ok, spec} = Bumblebee.load_spec(mistral, module: Bumblebee.Text.Mistral, architecture: :for_causal_language_modeling)&lt;/code&gt;
    &lt;code class=&quot;language-elixir&quot;&gt;&lt;/code&gt;
    &lt;code class=&quot;language-elixir&quot;&gt;  {:ok, model_info} = Bumblebee.load_model(mistral, spec: spec, backend: {EXLA.Backend, client: :host})&lt;/code&gt;
    &lt;code class=&quot;language-elixir&quot;&gt;  {:ok, tokenizer} = Bumblebee.load_tokenizer(mistral, module: Bumblebee.Text.LlamaTokenizer)&lt;/code&gt;
    &lt;code class=&quot;language-elixir&quot;&gt;  {:ok, generation_config} = Bumblebee.load_generation_config(mistral, spec_module: Bumblebee.Text.Mistral)&lt;/code&gt;
    &lt;code class=&quot;language-elixir&quot;&gt;&lt;/code&gt;
    &lt;code class=&quot;language-elixir&quot;&gt;  generation_config = Bumblebee.configure(generation_config, max_new_tokens: 500)&lt;/code&gt;
    &lt;code class=&quot;language-elixir&quot;&gt;  Bumblebee.Text.generation(model_info, tokenizer, generation_config, defn_options: [compiler: EXLA])&lt;/code&gt;
    &lt;code class=&quot;language-elixir&quot;&gt;end&lt;/code&gt;
  &lt;/pre&gt;
&lt;/div&gt;

&lt;p&gt;Next you can wire this up in your &lt;a href=&quot;https://github.com/toranb/pgvector-search/blob/mistral7b/lib/search/application.ex#L14&quot;&gt;application.ex&lt;/a&gt;&lt;/p&gt;

&lt;div class=&quot;highlight&quot; data-language=&quot;elixir&quot;&gt;
  &lt;pre class=&quot;language-elixir&quot;&gt;
    &lt;code class=&quot;language-elixir&quot;&gt;def start(_type, _args) do&lt;/code&gt;
    &lt;code class=&quot;language-elixir&quot;&gt;  children = [&lt;/code&gt;
    &lt;code class=&quot;language-elixir&quot;&gt;    {Nx.Serving, serving: serving(), name: ChatServing}&lt;/code&gt;
    &lt;code class=&quot;language-elixir&quot;&gt;  ]&lt;/code&gt;
    &lt;code class=&quot;language-elixir&quot;&gt;end&lt;/code&gt;
  &lt;/pre&gt;
&lt;/div&gt;

&lt;p&gt;You can prompt the model from &lt;a href=&quot;https://github.com/toranb/pgvector-search/blob/mistral7b/lib/search_web/live/page_live.ex#L44&quot;&gt;elixir code&lt;/a&gt; with &lt;a href=&quot;https://hexdocs.pm/nx/Nx.Serving.html&quot;&gt;Nx.Serving&lt;/a&gt;.&lt;/p&gt;

&lt;div class=&quot;highlight&quot; data-language=&quot;elixir&quot;&gt;
  &lt;pre class=&quot;language-elixir&quot;&gt;
    &lt;code class=&quot;language-elixir&quot;&gt;Nx.Serving.batched_run(ChatServing, prompt)&lt;/code&gt;
  &lt;/pre&gt;
&lt;/div&gt;

&lt;p&gt;With this fine tuned model up and running we can ask it to generate a MathJSON expression.&lt;/p&gt;

&lt;img style=&quot;margin-top: -10px; margin-bottom: -30px; width: 100%;&quot; src=&quot;/content/images/2023/examplemath.png&quot; border=&quot;0&quot;&gt;

&lt;p&gt;Finally, you can take this output from the model and verify it with help from mathjson_solver.&lt;/p&gt;

&lt;img style=&quot;margin-top: -10px; margin-bottom: -30px; width: 100%;&quot; src=&quot;/content/images/2023/solver.png&quot; border=&quot;0&quot;&gt;

&lt;p&gt;I want to give a big shout out to &lt;a href=&quot;https://twitter.com/jon_durbin&quot;&gt;Jon Durbin&lt;/a&gt; for creating the model that inspired this blog post, the &lt;a href=&quot;https://huggingface.co/datasets/jondurbin/airoboros-3.1/viewer/default/train?q=math+json&amp;row=402&quot;&gt;MathJSON dataset&lt;/a&gt; and for helping answer a great many questions I had along the way. I also want to thank &lt;a href=&quot;https://twitter.com/sean_moriarity/&quot;&gt;Sean Moriarity&lt;/a&gt; for his work &lt;a href=&quot;https://github.com/elixir-nx/bumblebee/pull/264&quot;&gt;implementing the Mistral 7B model in Bumblebee&lt;/a&gt; that made it possible to serve with Nx.&lt;/p&gt;
</description>
                <pubDate>Sat, 21 Oct 2023 01:00:00 +0000</pubDate>
                <link>http://toranbillups.com/blog/archive/2023/10/21/fine-tune-mistral-and-serve-with-nx/</link>
                <guid isPermaLink="true">http://toranbillups.com/blog/archive/2023/10/21/fine-tune-mistral-and-serve-with-nx/</guid>
            </item>
        
            <item>
                <title>Install CUDA 12 on PopOS</title>
                <description>&lt;p&gt;Yesterday I upgraded to the latest version of &lt;a href=&quot;https://github.com/elixir-nx/bumblebee&quot;&gt;bumblebee&lt;/a&gt; only to learn &lt;a href=&quot;https://github.com/elixir-nx/nx/tree/main/exla&quot;&gt;EXLA&lt;/a&gt; took on the latest XLA which dropped support for CUDA 11.2.&lt;/p&gt;

&lt;p&gt;Because I was running &lt;a href=&quot;https://support.system76.com/articles/install-pop/&quot;&gt;Pop!OS&lt;/a&gt; I assumed it would be something I could `apt install` and call it good. Unfortunately it was more work than my first go round so I wanted to detail it here for anyone who might follow. This is loosely based off the &lt;a href=&quot;https://gist.github.com/ksopyla/bf74e8ce2683460d8de6e0dc389fc7f5&quot;&gt;older CUDA 11 install instructions&lt;/a&gt; I found but updated for ubuntu 22.04.&lt;/p&gt;

&lt;p&gt;Before you get started downloading anything, &lt;a href=&quot;https://developer.nvidia.com/cudnn&quot;&gt;setup a developer account with nvidia&lt;/a&gt; so you can download cuDNN in a few minutes.&lt;/p&gt;

&lt;div class=&quot;highlight&quot; data-language=&quot;elixir&quot;&gt;
  &lt;pre class=&quot;language-elixir&quot;&gt;
    &lt;code class=&quot;language-elixir&quot;&gt;$ wget https://developer.download.nvidia.com/compute/cuda/repos/ubuntu2204/x86_64/cuda-ubuntu2204.pin&lt;/code&gt;
    &lt;code class=&quot;language-elixir&quot;&gt;$ sudo mv cuda-ubuntu2204.pin /etc/apt/preferences.d/cuda-repository-pin-600&lt;/code&gt;
    &lt;code class=&quot;language-elixir&quot;&gt;$ sudo apt-key adv --fetch-keys https://developer.download.nvidia.com/compute/cuda/repos/ubuntu2204/x86_64/3bf863cc.pub&lt;/code&gt;
    &lt;code class=&quot;language-elixir&quot;&gt;$ sudo add-apt-repository &quot;deb http://developer.download.nvidia.com/compute/cuda/repos/ubuntu2204/x86_64/ /&quot;&lt;/code&gt;
    &lt;code class=&quot;language-elixir&quot;&gt;$ sudo apt update&lt;/code&gt;
    &lt;code class=&quot;language-elixir&quot;&gt;$ sudo apt install cuda-toolkit-12-2&lt;/code&gt;
  &lt;/pre&gt;
&lt;/div&gt;

&lt;p&gt;Next download &lt;a href=&quot;https://developer.nvidia.com/rdp/cudnn-download&quot;&gt;cuDNN&lt;/a&gt; from nvidia and install it.&lt;/p&gt;

&lt;div class=&quot;highlight&quot; data-language=&quot;elixir&quot;&gt;
  &lt;pre class=&quot;language-elixir&quot;&gt;
    &lt;code class=&quot;language-elixir&quot;&gt;$ sudo dpkg -i cudnn-local-repo-ubuntu2204-8.9.4.25_1.0-1_amd64.deb&lt;/code&gt;
    &lt;code class=&quot;language-elixir&quot;&gt;$ sudo cp /var/cudnn-local-repo-ubuntu2204-8.9.4.25/cudnn-local-72322D7F-keyring.gpg /usr/share/keyrings/&lt;/code&gt;
    &lt;code class=&quot;language-elixir&quot;&gt;$ sudo apt update&lt;/code&gt;
    &lt;code class=&quot;language-elixir&quot;&gt;$ sudo apt install libcudnn8=8.9.4.25-1+cuda12.2&lt;/code&gt;
  &lt;/pre&gt;
&lt;/div&gt;

&lt;p&gt;Finally, set a few env variables and the correct XLA_TARGET for EXLA&lt;/p&gt;

&lt;div class=&quot;highlight&quot; data-language=&quot;elixir&quot;&gt;
  &lt;pre class=&quot;language-elixir&quot;&gt;
    &lt;code class=&quot;language-elixir&quot;&gt;export LD_LIBRARY_PATH=&quot;$LD_LIBRARY_PATH:/usr/local/cuda/lib64:/usr/local/cuda/extras/CUPTI/lib64&quot;&lt;/code&gt;
    &lt;code class=&quot;language-elixir&quot;&gt;export CUDA_HOME=/usr/local/cuda&lt;/code&gt;
    &lt;code class=&quot;language-elixir&quot;&gt;export PATH=&quot;/usr/local/cuda/bin:$PATH&quot;&lt;/code&gt;
    &lt;code class=&quot;language-elixir&quot;&gt;export XLA_TARGET=cuda120&lt;/code&gt;
    &lt;code class=&quot;language-elixir&quot;&gt;export XLA_FLAGS=--xla_gpu_cuda_data_dir=/usr/local/cuda&lt;/code&gt;
  &lt;/pre&gt;
&lt;/div&gt;

&lt;p&gt;Open a new terminal session and verify CUDA 12 is setup correctly by running this command&lt;/p&gt;

&lt;div class=&quot;highlight&quot; data-language=&quot;elixir&quot;&gt;
  &lt;pre class=&quot;language-elixir&quot;&gt;
    &lt;code class=&quot;language-elixir&quot;&gt;nvcc -V&lt;/code&gt;
  &lt;/pre&gt;
&lt;/div&gt;

&lt;p&gt;To upgrade from 8.9 download &lt;a href=&quot;https://developer.nvidia.com/cudnn-9-1-1-download-archive&quot;&gt;cuDNN 9.1&lt;/a&gt; from nvidia. Before you install it remove libcudnn8 as shown below.&lt;/p&gt;

&lt;div class=&quot;highlight&quot; data-language=&quot;elixir&quot;&gt;
  &lt;pre class=&quot;language-elixir&quot;&gt;
    &lt;code class=&quot;language-elixir&quot;&gt;$ sudo apt remove libcudnn8&lt;/code&gt;
    &lt;code class=&quot;language-elixir&quot;&gt;$ sudo dpkg -i cudnn-local-repo-ubuntu2204-9.1.1_1.0-1_amd64.deb&lt;/code&gt;
    &lt;code class=&quot;language-elixir&quot;&gt;$ sudo cp /var/cudnn-local-repo-ubuntu2204-9.1.1/cudnn-local-AD7F4AC5-keyring.gpg /usr/share/keyrings/&lt;/code&gt;
    &lt;code class=&quot;language-elixir&quot;&gt;$ sudo apt update&lt;/code&gt;
    &lt;code class=&quot;language-elixir&quot;&gt;$ sudo apt install libcudnn9-cuda-12=9.1.1.17-1&lt;/code&gt;
    &lt;code class=&quot;language-elixir&quot;&gt;$ sudo apt install cuda-toolkit-12-6&lt;/code&gt;
    &lt;code class=&quot;language-elixir&quot;&gt;$ sudo apt update&lt;/code&gt;
  &lt;/pre&gt;
&lt;/div&gt;

&lt;p&gt;After you upgrade to 9.1.1 if you ever want to downgrade to 8.9 follow these instructions.&lt;/p&gt;

&lt;div class=&quot;highlight&quot; data-language=&quot;elixir&quot;&gt;
  &lt;pre class=&quot;language-elixir&quot;&gt;
    &lt;code class=&quot;language-elixir&quot;&gt;$ sudo apt remove libcudnn9-cuda-12&lt;/code&gt;
    &lt;code class=&quot;language-elixir&quot;&gt;$ sudo dpkg -i cudnn-local-repo-ubuntu2204-8.9.7.29_1.0-1_amd64.deb&lt;/code&gt;
    &lt;code class=&quot;language-elixir&quot;&gt;$ sudo cp /var/cudnn-local-repo-ubuntu2204-8.9.7.29/cudnn-local-08A7D361-keyring.gpg /usr/share/keyrings/&lt;/code&gt;
    &lt;code class=&quot;language-elixir&quot;&gt;$ sudo apt update&lt;/code&gt;
    &lt;code class=&quot;language-elixir&quot;&gt;$ sudo apt install libcudnn8=8.9.7.29-1+cuda12.2&lt;/code&gt;
  &lt;/pre&gt;
&lt;/div&gt;

&lt;p&gt;The only difference between 8.9 and 9.1 is a small tweak to the XLA_TARGET in your zshrc. For Nx 7 you would normally use cuda120 but after upgrading to Nx 9 and CUDNN 9.1.1 you need cuda12 instead&lt;/p&gt;

&lt;div class=&quot;highlight&quot; data-language=&quot;elixir&quot;&gt;
  &lt;pre class=&quot;language-elixir&quot;&gt;
    &lt;code class=&quot;language-elixir&quot;&gt;XLA_TARGET=cuda12&lt;/code&gt;
  &lt;/pre&gt;
&lt;/div&gt;
</description>
                <pubDate>Sat, 19 Aug 2023 18:00:00 +0000</pubDate>
                <link>http://toranbillups.com/blog/archive/2023/08/19/install-cuda-12-on-popos/</link>
                <guid isPermaLink="true">http://toranbillups.com/blog/archive/2023/08/19/install-cuda-12-on-popos/</guid>
            </item>
        
            <item>
                <title>Training Axon Models With Nvidia GPUs</title>
                <description>&lt;p&gt;A few months back I started a deep dive into machine learning. With all the excitement about &lt;a href=&quot;https://github.com/elixir-nx/nx&quot;&gt;Nx&lt;/a&gt; I spent the first few weeks building a toy example that solves fizzbuzz, first with &lt;a href=&quot;https://gist.github.com/toranb/e5c48565e83e4baaaf2c5850531a8a58&quot;&gt;Axon&lt;/a&gt; and later with &lt;a href=&quot;https://gist.github.com/toranb/56a3e65ca81fba1c4f6c92c6f1857681&quot;&gt;Nx&lt;/a&gt;. After getting more familiar with &lt;a href=&quot;https://github.com/elixir-nx/axon&quot;&gt;Axon&lt;/a&gt; I started tinkering with the &lt;a href=&quot;https://huggingface.co/bert-base-cased&quot;&gt;BERT&lt;/a&gt; &lt;a href=&quot;https://hexdocs.pm/bumblebee/fine_tuning.html&quot;&gt;fine tuning example&lt;/a&gt; and found the feedback loop was 45+ minutes.&lt;/p&gt;

&lt;p&gt;I didn&apos;t have a huge budget but I knew a previous generation nvidia card like the &lt;a href=&quot;https://www.amazon.com/dp/B0971BG25M?psc=1&amp;ref=ppx_yo2ov_dt_b_product_details&quot;&gt;RTX 3060&lt;/a&gt; would improve the turnaround time allowing me to train models more quickly. After looking at some benchmarks and considering a few alternatives I decided to order the 12GB model and take it for a spin.&lt;/p&gt;

&lt;p&gt;I started by looking at Nvidia support for linux and decided to &lt;a href=&quot;https://support.system76.com/articles/install-pop/&quot;&gt;install Pop!OS&lt;/a&gt;. Next I installed &lt;a href=&quot;https://gist.github.com/toranb/93999c046d871c764f8db5f336dc2abe&quot;&gt;elixir with asdf&lt;/a&gt; to get a working dev enviornment before attempting to optimize it further. From a vanilla install of Pop!OS I found nothing was installed for me by default so I had to list the nvidia drivers and install the latest stable driver.&lt;/p&gt;

&lt;div class=&quot;highlight&quot; data-language=&quot;elixir&quot;&gt;
  &lt;pre class=&quot;language-elixir&quot;&gt;
    &lt;code class=&quot;language-elixir&quot;&gt;sudo ubuntu-drivers list&lt;/code&gt;
    &lt;code class=&quot;language-elixir&quot;&gt;sudo ubuntu-drivers install nvidia-driver-525&lt;/code&gt;
    &lt;code class=&quot;language-elixir&quot;&gt;sudo apt install system76-cuda-11.2 system76-cudnn-11.2&lt;/code&gt;
  &lt;/pre&gt;
&lt;/div&gt;

&lt;p&gt;Finally, I exported 2 environment variables that inform the runtime about the installed cuda version and path.&lt;/p&gt;

&lt;div class=&quot;highlight&quot; data-language=&quot;elixir&quot;&gt;
  &lt;pre class=&quot;language-elixir&quot;&gt;
    &lt;code class=&quot;language-elixir&quot;&gt;export XLA_TARGET=cuda111&lt;/code&gt;
    &lt;code class=&quot;language-elixir&quot;&gt;export XLA_FLAGS=--xla_gpu_cuda_data_dir=/usr/lib/cuda-11.2&lt;/code&gt;
  &lt;/pre&gt;
&lt;/div&gt;

&lt;p&gt;With this &lt;a href=&quot;https://elixir-lang.org/&quot;&gt;Elixir&lt;/a&gt; workstation you can generate training data, train your models and even &lt;a href=&quot;https://github.com/toranb/nx-serving-fizzbuzz/blob/main/lib/game/demo.ex&quot;&gt;serve them with Nx&lt;/a&gt;!&lt;/p&gt;

&lt;div style=&quot;padding-top: 20px; padding-bottom: 20px;&quot;&gt;
  &lt;div style=&quot;margin: auto; width: 90vw; height: 50vw; max-width: 768px; max-height: 432px&quot;&gt;
    &lt;iframe src=&quot;https://player.vimeo.com/video/822474762&quot; style=&quot;width:100%;height:100%;&quot; frameborder=&quot;0&quot; allow=&quot;autoplay; fullscreen; picture-in-picture&quot; allowfullscreen&gt;&lt;/iframe&gt;
  &lt;/div&gt;
  &lt;script src=&quot;https://player.vimeo.com/api/player.js&quot;&gt;&lt;/script&gt;
&lt;/div&gt;

&lt;p&gt;Despite all the promise and obvious speed improvements this GPU has to offer I found the fine tuning example I started my journey with throws &lt;a href=&quot;https://elixirforum.com/t/memory-issues-following-bumblebee-example/55391/5&quot;&gt;out of memory errors during the 2nd epoch&lt;/a&gt; because the RAM usage jumps to 32GB with this specific BERT model.&lt;/p&gt;

&lt;p&gt;I was however able to complete the fine tuning example with a slightly smaller BERT variant. Here is the &lt;a href=&quot;https://gist.github.com/toranb/b37096fee0c9af93d16b0aaa1a9bcdf4&quot;&gt;full source&lt;/a&gt; for that elixir module for those interested.&lt;/p&gt;
</description>
                <pubDate>Sat, 29 Apr 2023 18:00:00 +0000</pubDate>
                <link>http://toranbillups.com/blog/archive/2023/04/29/training-axon-models-with-nvidia-gpus/</link>
                <guid isPermaLink="true">http://toranbillups.com/blog/archive/2023/04/29/training-axon-models-with-nvidia-gpus/</guid>
            </item>
        
            <item>
                <title>Empowered Product Teams: ownership and responsibility</title>
                <description>&lt;p&gt;Throughout my career I&apos;ve had the opportunity to blend, extend and adapt well known engineering practices to match the accelerated demands put on product teams. After years of trial and error I&apos;ve become convinced empowerment is at the core of both better product teams and better software.&lt;/p&gt;

&lt;p&gt;Truly &lt;a href=&quot;https://www.goodreads.com/book/show/53481975-empowered&quot;&gt;empowered&lt;/a&gt; product teams are given more ownership to learn, adapt and respond to feedback. With this ownership comes responsibility that requires a combination of &lt;a href=&quot;https://www.goodreads.com/en/book/show/58046715-continuous-discovery-habits&quot;&gt;discovery&lt;/a&gt;, experimentation, and &lt;a href=&quot;https://basecamp.com/shapeup&quot;&gt;delivery&lt;/a&gt;. Engineering teams must then focus on outcomes instead of output, which better equips them to organize, shape and sequence the work.&lt;/p&gt;

&lt;p&gt;A strong product team must navigate uncertainty. Yet too often, we find ourselves crammed into a framework that views software creation not as a journey of discovery, but as a fixed process with segregated roles. By embracing this new approach to knowledge work, software careers can be more fulfilling, rewarding, and impactful.&lt;/p&gt;

&lt;div style=&quot;padding-top: 20px; padding-bottom: 20px;&quot;&gt;
  &lt;div style=&quot;margin: auto; width: 90vw; height: 50vw; max-width: 768px; max-height: 432px&quot;&gt;
    &lt;iframe src=&quot;https://player.vimeo.com/video/740207486&quot; style=&quot;width:100%;height:100%;&quot; frameborder=&quot;0&quot; allow=&quot;autoplay; fullscreen; picture-in-picture&quot; allowfullscreen&gt;&lt;/iframe&gt;
  &lt;/div&gt;
  &lt;script src=&quot;https://player.vimeo.com/api/player.js&quot;&gt;&lt;/script&gt;
&lt;/div&gt;
</description>
                <pubDate>Mon, 19 Sep 2022 18:00:00 +0000</pubDate>
                <link>http://toranbillups.com/blog/archive/2022/09/19/empowered-product-teams/</link>
                <guid isPermaLink="true">http://toranbillups.com/blog/archive/2022/09/19/empowered-product-teams/</guid>
            </item>
        
            <item>
                <title>A Philosophy of Software Design</title>
                <description>&lt;p&gt;Last week I started a new role and ahead of this new adventure I read &lt;a href=&quot;https://www.amazon.com/Philosophy-Software-Design-John-Ousterhout/dp/1732102201&quot;&gt;A Philosophy of Software Design&lt;/a&gt; with the specific purpose of gifting the book and a summary of it to each of my engineers. What follows is my paraphrased summary of the first 9 chapters for those who might find the topic interesting.&lt;/p&gt;

&lt;h3&gt;Chapter 1&lt;/h3&gt;

&lt;p&gt;The greatest limitation in writing software is our ability to understand the systems we are creating&lt;/p&gt;
&lt;p&gt;The larger the program, and the more people that work on it, the more difficult it is to manage complexity&lt;/p&gt;
&lt;p&gt;2 approaches to fighting complexity&lt;/p&gt;
&lt;ul&gt;
  &lt;li&gt;eliminate complexity by making code simpler and more obvious&lt;/li&gt;
  &lt;li&gt;encapsulate it so that programmers can work on a system without being exposed to all of it’s complexity at once&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;Chapter 2&lt;/h3&gt;

&lt;p&gt;Complexity is anything related to the structure of a software system that makes it hard to understand and modify the system&lt;/p&gt;
&lt;p&gt;The overall complexity of a system is defined by the complexity of each part multiplied by the amount of time developers spend working on that part&lt;/p&gt;
&lt;p&gt;Symptoms of complexity&lt;/p&gt;
&lt;ul&gt;
  &lt;li&gt;change amplification -a seemingly simple change requires code modifications in many different parts of the system&lt;/li&gt;
  &lt;li&gt;cognitive load -refers to how much a developer needs to know to complete a task&lt;/li&gt;
  &lt;li&gt;unknown unknowns -it’s not obvious what pieces of code should be modified to complete a task&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Causes of complexity&lt;/p&gt;
&lt;ul&gt;
  &lt;li&gt;dependencies -exists when a given piece of code cannot be understood and modified in isolation. We cannot eliminate dependencies so the goal is to have fewer and make it obvious how and where they are used&lt;/li&gt;
  &lt;li&gt;obscurity - when important information is not obvious &lt;/li&gt;
  &lt;li&gt;honorable mention - Inconsistency (as a contributor to obscurity)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Complexity comes from an accumulation of dependencies and obscurities &lt;/p&gt;

&lt;h3&gt;Chapter 3&lt;/h3&gt;

&lt;p&gt;Complexity is incremental&lt;/p&gt;
&lt;p&gt;Working code isn’t enough because it can introduce complexity if done purely tactical&lt;/p&gt;
&lt;p&gt;The more strategic approach strikes a balance between design and working code&lt;/p&gt;

&lt;h3&gt;Chapter 4&lt;/h3&gt;

&lt;p&gt;One of the most important techniques for managing complexity is to design systems so that developers only need to face a small fraction of the overall complexity at any given time&lt;/p&gt;
&lt;p&gt;Software systems are decomposed into a collection of modules that are relatively independent&lt;/p&gt;
&lt;p&gt;The arguments to a function create a dependency between the function and the call site&lt;/p&gt;
&lt;p&gt;The goal of modular design is to minimize the dependencies between modules&lt;/p&gt;
&lt;p&gt;The best modules provide an interface that is much simpler than its implementation&lt;/p&gt;
&lt;p&gt;If a developer needs to know a particular piece of information in order to use a module, that information is part of that modules interface&lt;/p&gt;
&lt;p&gt;The best modules are deep: they have a lot of functionality hidden behind a simple interface&lt;/p&gt;
&lt;p&gt;A deep module is a good abstraction because only a small fraction of its internal complexity is visible to the user&lt;/p&gt;
&lt;p&gt;A shallow module is one whose interface is relatively complex in comparison to the functionality that it provides&lt;/p&gt;
&lt;p&gt;&lt;b&gt;By separating the interface of a module from its implementation, we can hide the complexity of the implementation from the rest of the system&lt;/b&gt;&lt;/p&gt;

&lt;h3&gt;Chapter 5&lt;/h3&gt;

&lt;p&gt;The most important technique for achieving deep modules is information hiding&lt;/p&gt;
&lt;p&gt;Each module should encapsulate a few pieces of knowledge, which represent design decisions. And this knowledge is embedded in the modules implementation but does not appear in its interface&lt;/p&gt;
&lt;p&gt;Information hiding reduces complexity in 2 ways&lt;/p&gt;
&lt;ul&gt;
  &lt;li&gt;it simplifies the interface to a module reducing cognitive load on the developer using the module&lt;/li&gt;
  &lt;li&gt;it makes it easier to evolve the system. No dependencies so any change will effect only the one module&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The opposite of information hiding is information leakage. This occurs when a design decision is reflected in multiple modules. This creates a dependency between the modules&lt;/p&gt;
&lt;p&gt;If a piece of information is reflected in the interface for a module, then by definition it has been leaked; thus simpler interfaces tend to correlate with better information hiding&lt;/p&gt;
&lt;p&gt;Back door leakage (that which is not visible in the interface) is more harmful because it’s not obvious&lt;/p&gt;
&lt;p&gt;Information hiding can often be improved by making a module slightly larger&lt;/p&gt;
&lt;p&gt;ex: 2 modules need to be run in a specific order, combining the 2 can hide this complexity&lt;/p&gt;

&lt;h3&gt;Chapter 6&lt;/h3&gt;

&lt;p&gt;The modules functionality should reflect your current needs, but its interface should not. Instead the interface should be general enough to support multiple uses&lt;/p&gt;
&lt;p&gt;But don&apos;t get carried away and build something so general purpose that it is difficult to use&lt;/p&gt;
&lt;p&gt;The most important benefit of the general purpose approach is that it results in simpler and deeper interfaces when compared to the special purpose approach&lt;/p&gt;
&lt;p&gt;One of the most important elements of software design is determining who needs to know what, and when&lt;/p&gt;
&lt;p&gt;Questions to ask about the design&lt;/p&gt;
&lt;ul&gt;
  &lt;li&gt;what is the simplest interface that will cover all my current needs&lt;/li&gt;
  &lt;li&gt;in how many situations will this function be used&lt;/li&gt;
  &lt;li&gt;is this api easy to use for my current needs&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;General purpose interfaces provide a cleaner separation between modules, whereas special purpose interfaces tend to leak information between modules&lt;/p&gt;

&lt;h3&gt;Chapter 7&lt;/h3&gt;

&lt;p&gt;When adjacent layers have similar abstractions, the problem often manifests itself in the form of pass-through functions&lt;/p&gt;
&lt;p&gt;A pass-through function typically indicates that there is not a clean division of responsibility between the modules&lt;/p&gt;
&lt;p&gt;A pass-through function makes modules shallow; they increase the interface complexity of a module, which adds complexity without increasing the total functionality of the system&lt;/p&gt;

&lt;h3&gt;Chapter 8&lt;/h3&gt;

&lt;p&gt;Pulling complexity down makes the most sense when...&lt;/p&gt;
&lt;ul&gt;
  &lt;li&gt;the complexity pulled down is closely related to the modules existing functionality&lt;/li&gt;
  &lt;li&gt;it results in simplifications elsewhere in the application&lt;/li&gt;
  &lt;li&gt;it simplifies the modules interface&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;Chapter 9&lt;/h3&gt;

&lt;p&gt;The act of subdividing creates additional complexity that was not present before subdivision&lt;/p&gt;
&lt;ul&gt;
  &lt;li&gt;complexity can arise from the number of modules. The more you have, the more difficult it can be to keep track of them all and the more challenging it can be to find a desired component within the large collection (ie: more interfaces, every interface adds complexity)&lt;/li&gt;
  &lt;li&gt;you may need code to manage the collection of modules&lt;/li&gt;
  &lt;li&gt;subdivision can result in code duplication.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The act of combing code could be beneficial when...&lt;/p&gt;
&lt;ul&gt;
  &lt;li&gt;they share information&lt;/li&gt;
  &lt;li&gt;they overlap conceptually&lt;/li&gt;
  &lt;li&gt;it is difficult to understand one of the modules without also looking at the other&lt;/li&gt;
  &lt;li&gt;it results in a simpler interface for those who rely on the behavior(s)&lt;/li&gt;
&lt;/ul&gt;
</description>
                <pubDate>Tue, 05 Oct 2021 18:00:00 +0000</pubDate>
                <link>http://toranbillups.com/blog/archive/2021/10/05/philosophy-of-software-design/</link>
                <guid isPermaLink="true">http://toranbillups.com/blog/archive/2021/10/05/philosophy-of-software-design/</guid>
            </item>
        
            <item>
                <title>Crucial Conversations</title>
                <description>&lt;p&gt;This week I started a new role and for the first time I&apos;ve put team health at the forefront by re-reading &lt;a href=&quot;https://www.amazon.com/Crucial-Conversations-Talking-Stakes-Second/dp/1469266822&quot;&gt;Crucial Conversations&lt;/a&gt; with the specific purpose of gifting the book and a summary of it to each of my engineers. What follows is my paraphrased summary of the book, excluding the last 2 chapters, for those who might find the topic interesting.&lt;/p&gt;

&lt;h3&gt;Chapter 1&lt;/h3&gt;

&lt;p&gt;The ability to talk openly about high stakes, emotional, controversial topics&lt;/p&gt;
&lt;p&gt;The key skill of effective teams is the capacity to skillfully address emotionally and risky issues&lt;/p&gt;
&lt;p&gt;Despite the importance of this skill we often avoid these difficult conversations&lt;/p&gt;
&lt;p&gt;When we do engage our default response is often self defeating because &lt;/p&gt;
&lt;ul&gt;
  &lt;li&gt;We’ve not had great role models to draw from in our personal or professional lives&lt;/li&gt;
  &lt;li&gt;The reasoning capacity of our brain is cut in half as adrenaline starts pumping&lt;/li&gt;
  &lt;li&gt;It’s difficult to step back from the content and manage the flow of conversation&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Why study this topic? We only have 3 outcomes when faced with a crucial conversation&lt;/p&gt;
&lt;ul&gt;
  &lt;li&gt;We can avoid them&lt;/li&gt;
  &lt;li&gt;We can have them and handle them poorly&lt;/li&gt;
  &lt;li&gt;We can have them and handle them well&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;Chapter 2&lt;/h3&gt;

&lt;p&gt;The key to crucial conversations is effective dialog&lt;/p&gt;
&lt;p&gt;Dialog: the free flow of meaning between 2 or more people&lt;/p&gt;
&lt;p&gt;To be effective in dialog we seek to get all relevant information out into the open&lt;/p&gt;
&lt;p&gt;Beware: the fools choice may encourage silence -between telling the truth and loosing a friend&lt;/p&gt;
&lt;p&gt;At the beginning of a crucial conversation we usually don’t share the same pool of meaning&lt;/p&gt;
&lt;p&gt;As the pool of shared meaning grows&lt;/p&gt;
&lt;ul&gt;
  &lt;li&gt;Better choices can be made because those involved have more relevant information &lt;/li&gt;
  &lt;li&gt;With input from everyone you get increased ownership and unity for actions that follow&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;Chapter 3&lt;/h3&gt;

&lt;p&gt;To start, take a long hard look at yourself and recognize the role you play in dialog&lt;/p&gt;
&lt;p&gt;As much as others may need to change, the only person we can directly change is ourself&lt;/p&gt;
&lt;p&gt;Under fire we naturally resist complexity and stop adding to the pool of meaning&lt;/p&gt;
&lt;p&gt;When emotions run high we swap our original motive for &quot;winning&quot; or even &quot;punishing&quot;&lt;/p&gt;
&lt;p&gt;As you feel your motives shift ask yourself “what do I really want here?”&lt;/p&gt;
&lt;p&gt;Sometimes we choose personal safety over dialog by choosing silence&lt;/p&gt;
&lt;p&gt;We accept the certainty of bad results to avoid the possibility of uncomfortable conversation&lt;/p&gt;

&lt;h3&gt;Chapter 4&lt;/h3&gt;

&lt;p&gt;Dialog requires safety for all involved&lt;/p&gt;
&lt;p&gt;People will not add to the shared pool of meaning when they do not feel safe&lt;/p&gt;
&lt;p&gt;Learn to look for safety problems (dual processing) while in the conversation&lt;/p&gt;
&lt;p&gt;Beware: watching for conditions (ie: safety) and content at the same time takes practice&lt;/p&gt;
&lt;p&gt;People become defensive because of fear (the condition) not the content itself&lt;/p&gt;
&lt;p&gt;The problem is not the message, but when we fail to help others feel safe hearing the message&lt;/p&gt;
&lt;p&gt;You can absorb threatening feedback when you respect their opinion and trust their motives&lt;/p&gt;
&lt;p&gt;When you remove fear the brain can function with full reasoning capacity&lt;/p&gt;

&lt;h3&gt;Chapter 5&lt;/h3&gt;

&lt;p&gt;Safety requires commitment to a shared mutual purpose (the entry condition)&lt;/p&gt;
&lt;p&gt;To stay in dialog we need to maintain mutual respect (the continuance condition)&lt;/p&gt;
&lt;p&gt;When necessary, step out of the content, make it safe, then step back into the conversation&lt;/p&gt;
&lt;p&gt;Beware: don’t sugar coat or water down your message&lt;/p&gt;

&lt;p&gt;The 4 skills to establish a mutual purpose&lt;/p&gt;
&lt;ul&gt;
  &lt;li&gt;commit to seek mutual purpose (check your heart)&lt;/li&gt;
  &lt;li&gt;Recognize the purpose behind the strategy&lt;/li&gt;
  &lt;li&gt;Invent a mutual purpose&lt;/li&gt;
  &lt;li&gt;Brainstorm new strategies that serve all involved&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;To establish respect when violated&lt;/p&gt;
&lt;ul&gt;
  &lt;li&gt;apologize if you’ve truly disrespected someone&lt;/li&gt;
  &lt;li&gt;If respect was broken by misunderstanding, use contrasting to clarify the purpose or intent&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;Chapter 6&lt;/h3&gt;

&lt;p&gt;Emotions don’t just happen&lt;/p&gt;
&lt;p&gt;We feel something because of a thought we ourselves create&lt;/p&gt;
&lt;p&gt;We generally tell ourselves a story with partial information&lt;/p&gt;
&lt;p&gt;These “stories” help us give meaning so we can justify&lt;/p&gt;

&lt;p&gt;Once you’ve created the emotions you have 2 choices&lt;/p&gt;
&lt;p&gt;1) You can act on them&lt;/p&gt;
&lt;p&gt;2) Or be acted on by them&lt;/p&gt;
&lt;p&gt; &lt;/p&gt;
&lt;p&gt;To challenge the emotional response or story ask “what evidence do I have that supports this?”&lt;/p&gt;
&lt;p&gt;Beware: don’t confused stories with facts&lt;/p&gt;

&lt;p&gt;We generally tell 3 diff stories&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Victim stories &quot;not my fault&quot;&lt;/li&gt;
&lt;li&gt;Villain stories &quot;they have bad motives&quot;&lt;/li&gt;
&lt;li&gt;Helpless stories &quot;I’m powerless&quot;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Relax your absolute certainty long enough for dialog -the only reliable way to discover motive&lt;/p&gt;

&lt;h3&gt;Chapter 7&lt;/h3&gt;

&lt;p&gt;Speak honestly but with confidence, humility and skill&lt;/p&gt;
&lt;p&gt;Share the facts, not the conclusions&lt;/p&gt;
&lt;p&gt;Invite opposing views&lt;/p&gt;
&lt;p&gt;Tell your story (be sure this follows the facts)&lt;/p&gt;
&lt;p&gt;Ask for others paths&lt;/p&gt;
&lt;p&gt;Talk tentatively&lt;/p&gt;
&lt;p&gt;Encourage testing (of your views and opinions)&lt;/p&gt;

&lt;h3&gt;Chapter 8&lt;/h3&gt;

&lt;p&gt;The best way to influence is to use your ears&lt;/p&gt;
&lt;p&gt;When you invite others to share, you must mean it&lt;/p&gt;
&lt;p&gt;Be curious, ask questions to seek understanding&lt;/p&gt;
&lt;p&gt;Beware: we often start to insert incorrect motives&lt;/p&gt;
&lt;p&gt;When you sense this^ ask “why would a sane, rational person say this?”&lt;/p&gt;
&lt;p&gt;Retrace aloud -the other persons path to action (after you hear them explain it)&lt;/p&gt;
&lt;p&gt;Work to curb your reaction and return to the facts/story/emotion to seek understanding&lt;/p&gt;

&lt;p&gt;4 powerful listening skills&lt;/p&gt;
&lt;ul&gt;
  &lt;li&gt;Ask them to share their opinion&lt;/li&gt;
  &lt;li&gt;Mirror -when their tone or body language doesn’t match their words mirror it back to them&lt;/li&gt;
  &lt;li&gt;Paraphrase -repeat it back to clarify your own understanding&lt;/li&gt;
  &lt;li&gt;Prime -sometimes we pour into the shared pool to encourage them to do the same&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Keep in mind we are trying to understand their point of view -not necessarily agree with it&lt;/p&gt;
&lt;p&gt;Beware: its what you say and how you say it (keep your tone top of mind as you repeat back)&lt;/p&gt;

&lt;p&gt;Agree when you agree (don’t waste time debating if you don’t disagree)&lt;/p&gt;
&lt;p&gt;Build when key pieces of information are left out -grow the pool of shared meaning&lt;/p&gt;
&lt;p&gt;Compare when you differ and be open minded&lt;/p&gt;

&lt;h3&gt;Chapter 9&lt;/h3&gt;

&lt;p&gt;Decision making should be decided on up front ahead of the dialog itself&lt;/p&gt;
&lt;p&gt;To not violate expectations make it clear how the final decision will be made&lt;/p&gt;
&lt;p&gt;4 ways to make decisions (increasing degree of involvement)&lt;/p&gt;
&lt;ul&gt;
  &lt;li&gt;command - no involvement just delegate it&lt;/li&gt;
  &lt;li&gt;Consult -invite others to influence the decision&lt;/li&gt;
  &lt;li&gt;Vote - when several great options are present&lt;/li&gt;
  &lt;li&gt;Consensus -most involved but required when a unanimous decision is necessary &lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;To decide ask a few questions&lt;/p&gt;
&lt;ul&gt;
  &lt;li&gt;who cares? don’t invite people who aren’t involved&lt;/li&gt;
  &lt;li&gt;Who knows about this information? (to help with the shared pool, and decision making)&lt;/li&gt;
  &lt;li&gt;Who must agree to decide?&lt;/li&gt;
  &lt;li&gt;How many people is it worth involving&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;You want to avoid violated expectations and inaction (hold people accountable to promises)&lt;/p&gt;
</description>
                <pubDate>Mon, 27 Sep 2021 18:00:00 +0000</pubDate>
                <link>http://toranbillups.com/blog/archive/2021/09/27/crucial-conversations/</link>
                <guid isPermaLink="true">http://toranbillups.com/blog/archive/2021/09/27/crucial-conversations/</guid>
            </item>
        
            <item>
                <title>Elixir And Phoenix Upgrade Adventure</title>
                <description>&lt;p&gt;This week I finished an upgrade from &lt;a href=&quot;https://elixir-lang.org/&quot;&gt;Elixir&lt;/a&gt; 1.10 to 1.11.3 and more notably &lt;a href=&quot;https://www.phoenixframework.org/&quot;&gt;Phoenix&lt;/a&gt; 1.4 to 1.5.7. To get a sense of the risk I decided to upgrade Elixir first and keep the bulk of our dependencies as-is to measure what I was up against.&lt;/p&gt;

&lt;p&gt;Thankfully the Elixir upgrade, aside from a handful of compiler warnings, wasn&apos;t worth writing about. The downside of this easy upgrade was overconfidence which revealed itself as an inability to estimate the effort required to move forward with the latest Elixir, Phoenix, Ecto and &lt;a href=&quot;https://github.com/absinthe-graphql/absinthe&quot;&gt;Absinthe&lt;/a&gt; dependencies.&lt;/p&gt;

&lt;p&gt;I&apos;ve done platform upgrades like this in the past and usually failed to share any lessons learned with the wider community. This time around I decided to take the time and document my adventure to help anyone else who might find themselves in a similar situation.&lt;/p&gt;

&lt;p&gt;I&apos;ll be doing a brain dump of the most memorable problems I faced with enough detail to unblock others who might feel stuck like I did at times. I would have struggled much more if it wasn&apos;t for all the wonderful blog posts, issues and contributions in our community so to all who paid it forward &quot;Thank You!&quot;.&lt;/p&gt;

&lt;h3&gt;PubSub 2.0&lt;/h3&gt;

&lt;p&gt;The biggest breaking change was the &lt;a href=&quot;https://hexdocs.pm/phoenix_pubsub/Phoenix.PubSub.html&quot;&gt;PubSub&lt;/a&gt; upgrade from 1.0 to 2.0. I would guess most had no trouble because the &lt;a href=&quot;https://gist.github.com/chrismccord/e53e79ef8b34adf5d8122a47db44d22f&quot;&gt;guide&lt;/a&gt; was simple and straight forward. Just bump the dependencies, update the config and tweak the application.ex but for those of us who used the private api &lt;a href=&quot;https://github.com/phoenixframework/phoenix_pubsub/blob/1cfdb60c5df8ca55a5e1b60fe3d691234b68ad59/lib/phoenix/pubsub/local.ex#L160&quot;&gt;Phoenix.PubSub.Local.list&lt;/a&gt; ...well that&apos;s when things got interesting.&lt;/p&gt;

&lt;p&gt;At first glance PubSub 2.0 had a clear replacement for this functionality in &lt;a href=&quot;https://github.com/phoenixframework/phoenix_pubsub/blob/master/lib/phoenix/tracker.ex&quot;&gt;Phoenix.Tracker&lt;/a&gt;. The trouble was that Phoenix.Tracker centers around distributed presence tracking and what I needed was a metric about connected users for the local node to restore the &lt;a href=&quot;https://kubernetes.io/&quot;&gt;Kubernetes&lt;/a&gt; autoscaling solution my team put together last year.&lt;/p&gt;

&lt;p&gt;I threw together a simple &lt;a href=&quot;https://github.com/toranb/phx_pubsub_upgrade_demo/commit/87a2adfd6baa305489da0abe86591aaaab426a08&quot;&gt;demo&lt;/a&gt; app that shows the full solution for anyone who might be in a similar situation. TL;DR I used the &lt;a href=&quot;https://github.com/elixir-lang/elixir/blob/v1.11.3/lib/elixir/lib/registry.ex&quot;&gt;Registry&lt;/a&gt; to do the heavy lifting.&lt;/p&gt;

&lt;div class=&quot;highlight&quot; data-language=&quot;elixir&quot;&gt;
  &lt;pre class=&quot;language-elixir&quot;&gt;
    &lt;code class=&quot;language-elixir&quot;&gt;def handle_info(:after_join, %{assigns: %{session_id: session_id}} = socket) do&lt;/code&gt;
    &lt;code class=&quot;language-elixir&quot;&gt;  {:ok, _} = Registry.register(Subpub.Tracker.Registry, &quot;user_sockets&quot;, session_id)&lt;/code&gt;
    &lt;code class=&quot;language-elixir&quot;&gt;&lt;/code&gt;
    &lt;code class=&quot;language-elixir&quot;&gt;  {:noreply, socket}&lt;/code&gt;
    &lt;code class=&quot;language-elixir&quot;&gt;end&lt;/code&gt;
  &lt;/pre&gt;
&lt;/div&gt;

&lt;p&gt;Now I could use the Registry to surface this metric about the local node. Thankfully this was a drop in replacement for the behavior my team used in PubSub 1.0 and as a result the autoscaling feature was back in action with complete feature parity.&lt;/p&gt;

&lt;div class=&quot;highlight&quot; data-language=&quot;elixir&quot;&gt;
  &lt;pre class=&quot;language-elixir&quot;&gt;
    &lt;code class=&quot;language-elixir&quot;&gt;Registry.lookup(Subpub.Tracker.Registry, &quot;user_sockets&quot;)&lt;/code&gt;
  &lt;/pre&gt;
&lt;/div&gt;

&lt;h3&gt;handle_out/3 is undefined or private&lt;/h3&gt;

&lt;p&gt;Next was another PubSub related issue that showed itself at runtime when using Broadcast. Specifically, broadcasting to a Phoenix Channel without Phoenix. The biggest hurdle was the runtime error itself `handle_out/3 is undefined or private` which was not immediately obvious.&lt;/p&gt;

&lt;div class=&quot;highlight&quot; data-language=&quot;elixir&quot;&gt;
  &lt;pre class=&quot;language-elixir&quot;&gt;
    &lt;code class=&quot;language-elixir&quot;&gt;def broadcast(topic, event, payload) do&lt;/code&gt;
    &lt;code class=&quot;language-elixir&quot;&gt;  message = %Phoenix.Socket.Broadcast{&lt;/code&gt;
    &lt;code class=&quot;language-elixir&quot;&gt;    event: event,&lt;/code&gt;
    &lt;code class=&quot;language-elixir&quot;&gt;    payload: payload,&lt;/code&gt;
    &lt;code class=&quot;language-elixir&quot;&gt;    topic: topic&lt;/code&gt;
    &lt;code class=&quot;language-elixir&quot;&gt;  }&lt;/code&gt;
    &lt;code class=&quot;language-elixir&quot;&gt;&lt;/code&gt;
    &lt;code class=&quot;language-elixir&quot;&gt;  Phoenix.PubSub.direct_broadcast(Node.self(), My.PubSub, topic, message)&lt;/code&gt;
    &lt;code class=&quot;language-elixir&quot;&gt;end&lt;/code&gt;
  &lt;/pre&gt;
&lt;/div&gt;

&lt;p&gt;When I started searching around for clues on this I was lucky enough to find a forum post where Chris &lt;a href=&quot;https://elixirforum.com/t/broadcasting-to-phoenix-channel-without-phoenix/3514/4&quot;&gt;explains&lt;/a&gt; the problem in detail. Luckily the solution was simple enough, starting with the move away from `direct_broadcast` in favor of `local_broadcast`.&lt;/p&gt;

&lt;div class=&quot;highlight&quot; data-language=&quot;elixir&quot;&gt;
  &lt;pre class=&quot;language-elixir&quot;&gt;
    &lt;code class=&quot;language-elixir&quot;&gt;def broadcast(topic, event, payload) do&lt;/code&gt;
    &lt;code class=&quot;language-elixir&quot;&gt;  message = %Phoenix.Socket.Broadcast{&lt;/code&gt;
    &lt;code class=&quot;language-elixir&quot;&gt;    event: event,&lt;/code&gt;
    &lt;code class=&quot;language-elixir&quot;&gt;    payload: payload,&lt;/code&gt;
    &lt;code class=&quot;language-elixir&quot;&gt;    topic: topic&lt;/code&gt;
    &lt;code class=&quot;language-elixir&quot;&gt;  }&lt;/code&gt;
    &lt;code class=&quot;language-elixir&quot;&gt;&lt;/code&gt;
    &lt;code class=&quot;language-elixir&quot;&gt;  Phoenix.PubSub.local_broadcast(My.PubSub, topic, message)&lt;/code&gt;
    &lt;code class=&quot;language-elixir&quot;&gt;end&lt;/code&gt;
  &lt;/pre&gt;
&lt;/div&gt;

&lt;p&gt;Finally, I added a `handle_info` function to any channel that was used in this way to accomplish the push.&lt;/p&gt;

&lt;div class=&quot;highlight&quot; data-language=&quot;elixir&quot;&gt;
  &lt;pre class=&quot;language-elixir&quot;&gt;
    &lt;code class=&quot;language-elixir&quot;&gt;def handle_info(%{topic: _, event: event, payload: payload}, socket) do&lt;/code&gt;
    &lt;code class=&quot;language-elixir&quot;&gt;  push(socket, event, payload)&lt;/code&gt;
    &lt;code class=&quot;language-elixir&quot;&gt;&lt;/code&gt;
    &lt;code class=&quot;language-elixir&quot;&gt;  {:noreply, socket}&lt;/code&gt;
    &lt;code class=&quot;language-elixir&quot;&gt;end&lt;/code&gt;
  &lt;/pre&gt;
&lt;/div&gt;

&lt;h3&gt;Absinthe Ecto&lt;/h3&gt;

&lt;p&gt;Anyone familiar with open source knows that a young ecosystem will see some amount of churn and Elixir is no different. As part of the upgrade I found a handful of places the &lt;a href=&quot;https://github.com/absinthe-graphql/absinthe_ecto/blob/master/lib/absinthe/ecto.ex#L119&quot;&gt;assoc&lt;/a&gt; helper from &lt;a href=&quot;https://github.com/absinthe-graphql/absinthe_ecto&quot;&gt;absinthe_ecto&lt;/a&gt; was used for `belongs_to` and `has_many` relationships. As part of the Phoenix upgrade, version compatibility became a problem with this hex library because of the bump to ecto_sql 3.5.3.&lt;/p&gt;

&lt;div class=&quot;highlight&quot; data-language=&quot;elixir&quot;&gt;
  &lt;pre class=&quot;language-elixir&quot;&gt;
    &lt;code class=&quot;language-elixir&quot;&gt;object :post do&lt;/code&gt;
    &lt;code class=&quot;language-elixir&quot;&gt;  field :user, :user, resolve: assoc(:user)&lt;/code&gt;
    &lt;code class=&quot;language-elixir&quot;&gt;end&lt;/code&gt;
  &lt;/pre&gt;
&lt;/div&gt;

&lt;p&gt;Because the library is deprecated I removed it and pulled in the source code for &lt;a href=&quot;https://gist.github.com/toranb/2c0cc42f77c249ba8ff180b59da03614&quot;&gt;assoc&lt;/a&gt; to move forward. Side note: I did spend a few minutes looking at &lt;a href=&quot;https://github.com/absinthe-graphql/dataloader&quot;&gt;dataloader&lt;/a&gt; but decided to pull that in another day when I&apos;m battling n+1 issues.&lt;/p&gt;

&lt;h3&gt;Ecto&lt;/h3&gt;

&lt;p&gt;In many ways the upgrade to &lt;a href=&quot;https://github.com/elixir-ecto/ecto&quot;&gt;Ecto&lt;/a&gt; 3.5 was painless, but I did see a few instances that failed to compile.&lt;/p&gt;

&lt;div class=&quot;highlight&quot; data-language=&quot;elixir&quot;&gt;
  &lt;pre class=&quot;language-elixir&quot;&gt;
    &lt;code class=&quot;language-elixir&quot;&gt;schema &quot;posts&quot; do&lt;/code&gt;
    &lt;code class=&quot;language-elixir&quot;&gt;  field :comments, {:array, BrokenSchema.Comment}, virtual: true&lt;/code&gt;
    &lt;code class=&quot;language-elixir&quot;&gt;end&lt;/code&gt;
  &lt;/pre&gt;
&lt;/div&gt;

&lt;p&gt;I did find a useful &lt;a href=&quot;https://github.com/elixir-ecto/ecto/issues/3396&quot;&gt;github issue&lt;/a&gt; that &lt;a href=&quot;https://github.com/josevalim&quot;&gt;José Valim&lt;/a&gt; commented on.&lt;/p&gt;

&lt;blockquote&gt;The error message is correct, as Comment is not an Ecto.Type. I am actually surprised to how it worked before... &lt;/blockquote&gt;

&lt;p&gt;To workaround this I simply flipped to the `any` Ecto.Type and the compiler was happy.&lt;/p&gt;

&lt;div class=&quot;highlight&quot; data-language=&quot;elixir&quot;&gt;
  &lt;pre class=&quot;language-elixir&quot;&gt;
    &lt;code class=&quot;language-elixir&quot;&gt;schema &quot;posts&quot; do&lt;/code&gt;
    &lt;code class=&quot;language-elixir&quot;&gt;  field :comments, {:array, any}, virtual: true&lt;/code&gt;
    &lt;code class=&quot;language-elixir&quot;&gt;end&lt;/code&gt;
  &lt;/pre&gt;
&lt;/div&gt;

&lt;h3&gt;JSON Serialization&lt;/h3&gt;

&lt;p&gt;The most frustrating part of the upgrade was the move from &lt;a href=&quot;https://github.com/devinus/poison&quot;&gt;Poison&lt;/a&gt; to &lt;a href=&quot;https://hexdocs.pm/jason/readme.html&quot;&gt;Jason&lt;/a&gt; for serialization. Because so much of the Phoenix app is consumed by another client I ran into several stumbling blocks worth mentioning.&lt;/p&gt;

&lt;p&gt;The first problem was a runtime error `(FunctionClauseError) no function clause matching in Jason.Encoder` when a field on a given Ecto schema didn&apos;t have the correct type during serialization. The biggest challenge with this error is that it failed to offer much information about how to resolve it. I did find a &lt;a href=&quot;https://github.com/michalmuskala/jason/issues/120&quot;&gt;github issue&lt;/a&gt; from Oct 2020 that got the ball rolling. And not long after a &lt;a href=&quot;https://github.com/michalmuskala/jason/pull/123&quot;&gt;pull request&lt;/a&gt; was merged to give these warnings at compile time.&lt;/p&gt;

&lt;p&gt;The only trouble with this was that no public version was published so I pulled down the source code for the latest &lt;a href=&quot;https://github.com/michalmuskala/jason/blob/master/lib/encoder.ex&quot;&gt;encoder&lt;/a&gt; and used it locally to leverage the compiler. You could also reference the github commit SHA if you prefer. Either way, having these errors at compile time was a game changer so huge thanks to the team for recognizing this and working to resolve it quickly.&lt;/p&gt;

&lt;p&gt;The next problem was a runtime error `cannot encode association :comments from Post to JSON because the association was not loaded`. The simple workaround for this problem was to add the association to the `except` list for `derive` but the real problem was how often this had to be done and how painful it was to find all of the edge cases.&lt;/p&gt;

&lt;div class=&quot;highlight&quot; data-language=&quot;elixir&quot;&gt;
  &lt;pre class=&quot;language-elixir&quot;&gt;
    &lt;code class=&quot;language-elixir&quot;&gt;@derive {Jason.Encoder, except: [:__meta__, :comments]}&lt;/code&gt;
  &lt;/pre&gt;
&lt;/div&gt;

&lt;p&gt;In sharing about this, I hope someone from the community can suggest a better solution. At worst, I found all the places I sorely needed controller tests to catch regressions like this.&lt;/p&gt;

&lt;p&gt;The last problem was that previously all keys would be serialized for any embedded_schema even if the key wasn&apos;t explicitly declared as a field. This was trouble because more times than I&apos;d like to admit, `Map.put` was used to insert a new key and value that wasn&apos;t declared meaning that at runtime those values wouldn&apos;t show up in the json.&lt;/p&gt;

&lt;div class=&quot;highlight&quot; data-language=&quot;elixir&quot;&gt;
  &lt;pre class=&quot;language-elixir&quot;&gt;
    &lt;code class=&quot;language-elixir&quot;&gt;embedded_schema do&lt;/code&gt;
    &lt;code class=&quot;language-elixir&quot;&gt;  field :title&lt;/code&gt;
    &lt;code class=&quot;language-elixir&quot;&gt;end&lt;/code&gt;
  &lt;/pre&gt;
&lt;/div&gt;

&lt;div class=&quot;highlight&quot; data-language=&quot;elixir&quot;&gt;
  &lt;pre class=&quot;language-elixir&quot;&gt;
    &lt;code class=&quot;language-elixir&quot;&gt;def make_fun(data) do&lt;/code&gt;
    &lt;code class=&quot;language-elixir&quot;&gt;  data |&gt; Map.put(:author, user)&lt;/code&gt;
    &lt;code class=&quot;language-elixir&quot;&gt;end&lt;/code&gt;
  &lt;/pre&gt;
&lt;/div&gt;

&lt;p&gt;Thankfully the solution was easy enough. You just declare the field on the embedded_schema.&lt;/p&gt;

&lt;div class=&quot;highlight&quot; data-language=&quot;elixir&quot;&gt;
  &lt;pre class=&quot;language-elixir&quot;&gt;
    &lt;code class=&quot;language-elixir&quot;&gt;embedded_schema do&lt;/code&gt;
    &lt;code class=&quot;language-elixir&quot;&gt;  field :title&lt;/code&gt;
    &lt;code class=&quot;language-elixir&quot;&gt;  field :author&lt;/code&gt;
    &lt;code class=&quot;language-elixir&quot;&gt;end&lt;/code&gt;
  &lt;/pre&gt;
&lt;/div&gt;

&lt;h3&gt;Absinthe&lt;/h3&gt;

&lt;p&gt;One last problem was a wide-spread runtime error in the graphQL types. Previously a type of `:integer` would return a value like `10.4` without any trouble. But with the latest dependencies this would throw an error because of the type mismatch.&lt;/p&gt;

&lt;div class=&quot;highlight&quot; data-language=&quot;elixir&quot;&gt;
  &lt;pre class=&quot;language-elixir&quot;&gt;
    &lt;code class=&quot;language-elixir&quot;&gt;object :user do&lt;/code&gt;
    &lt;code class=&quot;language-elixir&quot;&gt;  field :some_avg, :integer&lt;/code&gt;
    &lt;code class=&quot;language-elixir&quot;&gt;end&lt;/code&gt;
  &lt;/pre&gt;
&lt;/div&gt;

&lt;p&gt;You can decide what type to use here for the specific domain you are working in. For simple averages it was a toss up between using string, to ensure accuracy, and float. Either way, the solution is easy enough - just change the type in the Absinthe type declaration.&lt;/p&gt;

&lt;div class=&quot;highlight&quot; data-language=&quot;elixir&quot;&gt;
  &lt;pre class=&quot;language-elixir&quot;&gt;
    &lt;code class=&quot;language-elixir&quot;&gt;object :user do&lt;/code&gt;
    &lt;code class=&quot;language-elixir&quot;&gt;  field :some_avg, :float&lt;/code&gt;
    &lt;code class=&quot;language-elixir&quot;&gt;end&lt;/code&gt;
  &lt;/pre&gt;
&lt;/div&gt;

&lt;h3&gt;Thank You!&lt;/h3&gt;

&lt;p&gt;Thanks for reading and I hope someone else can move quickly as a result of my sharing this. Huge thanks to the community and core teams for making Elixir a great ecosystem!&lt;/p&gt;
</description>
                <pubDate>Sun, 17 Jan 2021 18:00:00 +0000</pubDate>
                <link>http://toranbillups.com/blog/archive/2021/01/17/elixir-and-phoenix-upgrade-adventure/</link>
                <guid isPermaLink="true">http://toranbillups.com/blog/archive/2021/01/17/elixir-and-phoenix-upgrade-adventure/</guid>
            </item>
        
            <item>
                <title>Knowledge Work 3.0</title>
                <description>&lt;p&gt;This year I read &lt;a href=&quot;https://basecamp.com/shapeup&quot;&gt;Shape Up&lt;/a&gt;, &lt;a href=&quot;https://teamtopologies.com/book&quot;&gt;Team Topologies&lt;/a&gt; and a slightly more academic book on the topic of lean called &lt;a href=&quot;https://www.goodreads.com/book/show/6278270-the-principles-of-product-development-flow&quot;&gt;The Principles of Product Development Flow&lt;/a&gt; in an effort to learn more about delivery engineering. After weeks of careful study, I combined my takeaways with some practical work experience to produce a talk on the subject.&lt;/p&gt;

&lt;div style=&quot;padding-top: 20px; padding-bottom: 20px;&quot;&gt;
  &lt;div style=&quot;margin: auto; width: 90vw; height: 50vw; max-width: 768px; max-height: 432px&quot;&gt;
    &lt;iframe src=&quot;https://player.vimeo.com/video/458384193&quot; style=&quot;width:100%;height:100%;&quot; frameborder=&quot;0&quot; allow=&quot;autoplay; fullscreen; picture-in-picture&quot; allowfullscreen&gt;&lt;/iframe&gt;
  &lt;/div&gt;
  &lt;script src=&quot;https://player.vimeo.com/api/player.js&quot;&gt;&lt;/script&gt;
&lt;/div&gt;
</description>
                <pubDate>Tue, 15 Sep 2020 18:00:00 +0000</pubDate>
                <link>http://toranbillups.com/blog/archive/2020/09/15/knowledge-work-3/</link>
                <guid isPermaLink="true">http://toranbillups.com/blog/archive/2020/09/15/knowledge-work-3/</guid>
            </item>
        
            <item>
                <title>Cookie Authentication with Phoenix LiveView</title>
                <description>&lt;p&gt;When I started learning &lt;a href=&quot;https://elixir-lang.org/&quot;&gt;Elixir&lt;/a&gt; a few years ago I built a &lt;a href=&quot;https://github.com/toranb/elixir-match&quot;&gt;multiplayer game&lt;/a&gt; for the iPad using &lt;a href=&quot;https://www.phoenixframework.org/&quot;&gt;Phoenix&lt;/a&gt;. The game had a fairly minimal authentication requirement so I looked around at some of the popular open source libraries available but ultimatly I decided to write something myself to get experience with the language and ecosystem.&lt;/p&gt;

&lt;p&gt;Earlier this year I started getting more serious about &lt;a href=&quot;https://www.phoenixframework.org/blog/build-a-real-time-twitter-clone-in-15-minutes-with-live-view-and-phoenix-1-5&quot;&gt;Phoenix LiveView&lt;/a&gt; and quickly discovered login forms present a unique challenge because &lt;a href=&quot;https://github.com/toranb/elixir-match/blob/3cfe1f75cef401ef5c6b7dd72ebb6e99e90629ea/app/lib/match_web/controllers/session_controller.ex#L29-L31&quot;&gt;Plug.Conn.put_session&lt;/a&gt; isn&apos;t available from any `handle_event` callback. This was an important detail because &lt;a href=&quot;https://toranbillups.com/blog/archive/2018/11/18/implementing-basic-authentication/&quot;&gt;my first&lt;/a&gt; &lt;a href=&quot;https://github.com/toranb/elixir-match/blob/3cfe1f75cef401ef5c6b7dd72ebb6e99e90629ea/app/lib/match_web/router.ex#L24&quot;&gt;plug pipeline&lt;/a&gt; used &lt;a href=&quot;https://github.com/toranb/elixir-match/blob/3cfe1f75cef401ef5c6b7dd72ebb6e99e90629ea/app/lib/match_web/controllers/session_controller.ex#L30&quot;&gt;Plug.Conn.put_session&lt;/a&gt; to signal that the user had been authenticated.&lt;/p&gt;

&lt;p&gt;Like any challenging technical problem I decided to get creative and come up with something that would unlock this for my needs and potentially the needs of others who might be searching for answers like I was recently.&lt;/p&gt;

&lt;p&gt;TL;DR If you prefer to skip the story all the relevant code is available on &lt;a href=&quot;https://github.com/toranb/cookie-authentication-phoenix-liveview-example/commit/863e3c3b7cf4f2a0230c4f036d8a10183adcdc5b&quot;&gt;github&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;Cookies&lt;/h3&gt;

&lt;p&gt;Before I begin writing about the LiveView solution I need to share some about how Phoenix identifies the user for a given web request. By default the &lt;a href=&quot;https://github.com/toranb/cookie-authentication-phoenix-liveview-example/blob/master/lib/shop_web/endpoint.ex&quot;&gt;Endpoint&lt;/a&gt; in your Phoenix application includes &lt;a href=&quot;https://hexdocs.pm/plug/Plug.Session.html&quot;&gt;Plug.Session&lt;/a&gt; configured to use the &lt;a href=&quot;https://hexdocs.pm/plug/Plug.Session.COOKIE.html&quot;&gt;cookie session store&lt;/a&gt;.&lt;/p&gt;

&lt;div class=&quot;highlight&quot; data-language=&quot;elixir&quot;&gt;
  &lt;pre class=&quot;language-elixir&quot;&gt;
    &lt;code class=&quot;language-elixir&quot;&gt;defmodule ShopWeb.Endpoint do&lt;/code&gt;
    &lt;code class=&quot;language-elixir&quot;&gt;  use Phoenix.Endpoint, otp_app: :shop&lt;/code&gt;
    &lt;code class=&quot;language-elixir&quot;&gt;  &lt;/code&gt;
    &lt;code class=&quot;language-elixir&quot;&gt;  # The session will be stored in the cookie and signed,&lt;/code&gt;
    &lt;code class=&quot;language-elixir&quot;&gt;  # this means its contents can be read but not tampered with.&lt;/code&gt;
    &lt;code class=&quot;language-elixir&quot;&gt;  # Set :encryption_salt if you would also like to encrypt it.&lt;/code&gt;
    &lt;code class=&quot;language-elixir&quot;&gt;  @session_options [&lt;/code&gt;
    &lt;code class=&quot;language-elixir&quot;&gt;    store: :cookie,&lt;/code&gt;
    &lt;code class=&quot;language-elixir&quot;&gt;    key: &quot;_shop_key&quot;,&lt;/code&gt;
    &lt;code class=&quot;language-elixir&quot;&gt;    signing_salt: &quot;bWk6pxHd&quot;&lt;/code&gt;
    &lt;code class=&quot;language-elixir&quot;&gt;  ]&lt;/code&gt;
    &lt;code class=&quot;language-elixir&quot;&gt;  &lt;/code&gt;
    &lt;code class=&quot;language-elixir&quot;&gt;  plug Plug.Session, @session_options&lt;/code&gt;
    &lt;code class=&quot;language-elixir&quot;&gt;end&lt;/code&gt;
  &lt;/pre&gt;
&lt;/div&gt;

&lt;p&gt;To see this in action spin up the server with iex and make a request to localhost.&lt;/p&gt;

&lt;div class=&quot;highlight&quot; data-language=&quot;elixir&quot;&gt;
  &lt;pre class=&quot;language-elixir&quot;&gt;
    &lt;code class=&quot;language-elixir&quot;&gt;iex -S mix phx.server&lt;/code&gt;
    &lt;code class=&quot;language-elixir&quot;&gt;&lt;/code&gt;
    &lt;code class=&quot;language-elixir&quot;&gt;curl http://localhost:4000 --verbose&lt;/code&gt;
  &lt;/pre&gt;
&lt;/div&gt;

&lt;p&gt;Using the `verbose` flag with curl I saw the response from Phoenix includes a &lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Set-Cookie&quot;&gt;Set-Cookie&lt;/a&gt; header.&lt;/p&gt;

&lt;blockquote&gt;The Set-Cookie HTTP response header is used to send cookies from the server to the user agent, so the user agent can send them back to the server later. -MDN Web Docs&lt;/blockquote&gt;

&lt;p&gt;For &lt;a href=&quot;https://baekdal.com/thoughts/the-original-cookie-specification-from-1997-was-gdpr-compliant/&quot;&gt;better or worse&lt;/a&gt; this cookie is all that&apos;s needed to identify the user&apos;s session. Now that I understood where this cookie originated from, my attention quickly shifted to decoding the value so I could make sense of it.&lt;/p&gt;

&lt;div class=&quot;highlight&quot; data-language=&quot;elixir&quot;&gt;
  &lt;pre class=&quot;language-elixir&quot;&gt;
    &lt;code class=&quot;language-elixir&quot;&gt;set-cookie: _shop_key=SFMyNTY.g3QAAAABbQAAAAtfY3NyZl90b2tlbm0AAAAYZFRuNUtQMkJ5YWtKT1JnWUtCeXhmNmdP.l0T3G-i8I5dMwz7lEZnQAeK_WeqEZTxcDeyNY2poz_M; path=/; HttpOnly &lt;/code&gt;
  &lt;/pre&gt;
&lt;/div&gt;

&lt;p&gt;I found a great in depth blog post on &lt;a href=&quot;https://bitcrowd.dev/decoding-phoenix-session-cookies&quot;&gt;decoding Phoenix session cookies&lt;/a&gt; and the TL;DR looks something like this.&lt;/p&gt;

&lt;div class=&quot;highlight&quot; data-language=&quot;elixir&quot;&gt;
  &lt;pre class=&quot;language-elixir&quot;&gt;
    &lt;code class=&quot;language-elixir&quot;&gt;set_cookie = &quot;SFMyNTY.g3QAAAABbQAAAAtfY3NyZl90b2tlbm0AAAAYZFRuNUtQMkJ5YWtKT1JnWUtCeXhmNmdP.l0T3G-i8I5dMwz7lEZnQAeK_WeqEZTxcDeyNY2poz_M&quot;&lt;/code&gt;
    &lt;code class=&quot;language-elixir&quot;&gt;[_, payload, _] = String.split(set_cookie, &quot;.&quot;, parts: 3)&lt;/code&gt;
    &lt;code class=&quot;language-elixir&quot;&gt;{:ok, encoded_term } = Base.url_decode64(payload, padding: false)&lt;/code&gt;
    &lt;code class=&quot;language-elixir&quot;&gt;:erlang.binary_to_term(encoded_term)&lt;/code&gt;
  &lt;/pre&gt;
&lt;/div&gt;

&lt;p&gt;For the initial request I found the value only contained a &lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Glossary/CSRF&quot;&gt;CSRF&lt;/a&gt; token.&lt;/p&gt;

&lt;div class=&quot;highlight&quot; data-language=&quot;elixir&quot;&gt;
  &lt;pre class=&quot;language-elixir&quot;&gt;
    &lt;code class=&quot;language-elixir&quot;&gt;%{&quot;_csrf_token&quot; =&gt; &quot;GadhekDDOc28OZVc3tOfzQ==&quot;}&lt;/code&gt;
  &lt;/pre&gt;
&lt;/div&gt;

&lt;p&gt;After I got my head around all the moving parts I quickly started searching for ways to alter this cookie so that I could identify the session for each incoming request.&lt;/p&gt;

&lt;h3&gt;Server Rendered Authentication&lt;/h3&gt;

&lt;p&gt;Adding data to the cookie is simple with `Plug.Conn.put_session` because the session store is configured for this by default with Phoenix.&lt;/p&gt;

&lt;div class=&quot;highlight&quot; data-language=&quot;elixir&quot;&gt;
  &lt;pre class=&quot;language-elixir&quot;&gt;
    &lt;code class=&quot;language-elixir&quot;&gt;conn |&gt; put_session(:user_id, id)&lt;/code&gt;
  &lt;/pre&gt;
&lt;/div&gt;

&lt;p&gt;With a traditional server rendered app I would verify username/password in the POST controller action for &lt;a href=&quot;https://github.com/toranb/elixir-match/blob/master/app/lib/match_web/controllers/session_controller.ex#L23-L32&quot;&gt;login&lt;/a&gt;. When the username/password was correct I&apos;d insert something to identify the user.&lt;/p&gt;

&lt;div class=&quot;highlight&quot; data-language=&quot;elixir&quot;&gt;
  &lt;pre class=&quot;language-elixir&quot;&gt;
    &lt;code class=&quot;language-elixir&quot;&gt;case Login.authenticate_user(username, password) do&lt;/code&gt;
    &lt;code class=&quot;language-elixir&quot;&gt;  nil -&gt;&lt;/code&gt;
    &lt;code class=&quot;language-elixir&quot;&gt;    conn&lt;/code&gt;
    &lt;code class=&quot;language-elixir&quot;&gt;    |&gt; put_flash(:error, &quot;incorrect username or password&quot;)&lt;/code&gt;
    &lt;code class=&quot;language-elixir&quot;&gt;    |&gt; render(&quot;fail.html&quot;)&lt;/code&gt;
    &lt;code class=&quot;language-elixir&quot;&gt;  id -&gt;&lt;/code&gt;
    &lt;code class=&quot;language-elixir&quot;&gt;    conn&lt;/code&gt;
    &lt;code class=&quot;language-elixir&quot;&gt;    |&gt; put_session(:user_id, id)&lt;/code&gt;
    &lt;code class=&quot;language-elixir&quot;&gt;    |&gt; redirect(to: some_path)&lt;/code&gt;
    &lt;code class=&quot;language-elixir&quot;&gt;end&lt;/code&gt;
  &lt;/pre&gt;
&lt;/div&gt;

&lt;blockquote&gt;Note: relying on the user id like this should be considered nieve and exists solely for the purpose of illustrating the life cycle of the request for authentication at it&apos;s most basic level.&lt;/blockquote&gt;

&lt;p&gt;With this user id in the cookie I was then able to write a &lt;a href=&quot;https://github.com/toranb/elixir-match/blob/master/app/lib/match_web/authenticator.ex&quot;&gt;plug&lt;/a&gt; that would look to see if any user id was present and if so, I would add it to `conn` for consistent use throughout the request life cycle.&lt;/p&gt;

&lt;div class=&quot;highlight&quot; data-language=&quot;elixir&quot;&gt;
  &lt;pre class=&quot;language-elixir&quot;&gt;
    &lt;code class=&quot;language-elixir&quot;&gt;case get_session(conn, :user_id) do&lt;/code&gt;
    &lt;code class=&quot;language-elixir&quot;&gt;  nil -&gt;&lt;/code&gt;
    &lt;code class=&quot;language-elixir&quot;&gt;    conn&lt;/code&gt;
    &lt;code class=&quot;language-elixir&quot;&gt;  id -&gt;&lt;/code&gt;
    &lt;code class=&quot;language-elixir&quot;&gt;    assign(conn, :current_user_id, id)&lt;/code&gt;
    &lt;code class=&quot;language-elixir&quot;&gt;end&lt;/code&gt;
  &lt;/pre&gt;
&lt;/div&gt;

&lt;p&gt;Next I constructed a &lt;a href=&quot;https://github.com/toranb/elixir-match/blob/master/app/lib/match_web/router.ex#L6-L16&quot;&gt;plug&lt;/a&gt; that would redirect the user if `current_user_id` was not found. This redirect plug only runs for restricted parts of the site, unlike the plug above which runs for every request.&lt;/p&gt;

&lt;div class=&quot;highlight&quot; data-language=&quot;elixir&quot;&gt;
  &lt;pre class=&quot;language-elixir&quot;&gt;
    &lt;code class=&quot;language-elixir&quot;&gt;def redirect_unauthorized(conn, _opts) do&lt;/code&gt;
    &lt;code class=&quot;language-elixir&quot;&gt;  current_user_id = Map.get(conn.assigns, :current_user_id)&lt;/code&gt;
    &lt;code class=&quot;language-elixir&quot;&gt;  if current_user_id != nil do&lt;/code&gt;
    &lt;code class=&quot;language-elixir&quot;&gt;    conn&lt;/code&gt;
    &lt;code class=&quot;language-elixir&quot;&gt;  else&lt;/code&gt;
    &lt;code class=&quot;language-elixir&quot;&gt;    conn&lt;/code&gt;
    &lt;code class=&quot;language-elixir&quot;&gt;    |&gt; redirect(to: login_path(conn, :index))&lt;/code&gt;
    &lt;code class=&quot;language-elixir&quot;&gt;    |&gt; halt()&lt;/code&gt;
    &lt;code class=&quot;language-elixir&quot;&gt;end&lt;/code&gt;
  &lt;/pre&gt;
&lt;/div&gt;

&lt;h3&gt;The Workaround&lt;/h3&gt;

&lt;p&gt;The trouble with LiveView in this situation is that event handlers only have access to the web socket and cannot use `Plug.Conn.put_session`. To work around this limitation I use &lt;a href=&quot;https://elixir-lang.org/getting-started/mix-otp/ets.html&quot;&gt;ETS&lt;/a&gt; to accomplish the same end result.&lt;/p&gt;

&lt;p&gt;Like the traditional server rendered example I verify username/password in the &lt;a href=&quot;https://github.com/toranb/cookie-authentication-phoenix-liveview-example/blob/0f7d2b58b6be7208039d8e40d2aa2eed041b7dfa/lib/shop_web/live/login_live.ex#L124-L129&quot;&gt;event handler&lt;/a&gt;. When the username/password is correct I insert the user id into ETS and redirect the user.&lt;/p&gt;

&lt;div class=&quot;highlight&quot; data-language=&quot;elixir&quot;&gt;
  &lt;pre class=&quot;language-elixir&quot;&gt;
    &lt;code class=&quot;language-elixir&quot;&gt;case Login.authenticate_user(changeset) do&lt;/code&gt;
    &lt;code class=&quot;language-elixir&quot;&gt;  %Shop.User{id: user_id} -&gt;&lt;/code&gt;
    &lt;code class=&quot;language-elixir&quot;&gt;    :ets.insert(:shop_auth_table, {:user_id, &quot;#{user_id}&quot;})&lt;/code&gt;
    &lt;code class=&quot;language-elixir&quot;&gt;&lt;/code&gt;
    &lt;code class=&quot;language-elixir&quot;&gt;    redirect = socket |&gt; redirect(to: some_path)&lt;/code&gt;
    &lt;code class=&quot;language-elixir&quot;&gt;    {:noreply, redirect}&lt;/code&gt;
    &lt;code class=&quot;language-elixir&quot;&gt;&lt;/code&gt;
    &lt;code class=&quot;language-elixir&quot;&gt;  changeset -&gt;&lt;/code&gt;
    &lt;code class=&quot;language-elixir&quot;&gt;    {:noreply, assign(socket, changeset: changeset)}&lt;/code&gt;
    &lt;code class=&quot;language-elixir&quot;&gt;end&lt;/code&gt;
  &lt;/pre&gt;
&lt;/div&gt;

&lt;blockquote&gt;Note: as with the server rendered example, relying on the user id like this should be considered nieve and exists solely for the purpose of exploring a workaround.&lt;/blockquote&gt;

&lt;p&gt;With the user id in ETS I was then able to write a &lt;a href=&quot;https://github.com/toranb/cookie-authentication-phoenix-liveview-example/blob/0f7d2b58b6be7208039d8e40d2aa2eed041b7dfa/lib/shop_web/plugs/session.ex#L18-L26&quot;&gt;plug&lt;/a&gt; that would look to see if any user id was present and if so, I would add it to `conn` for consistent use throughout the request life cycle just as I did in the server rendered plug.&lt;/p&gt;

&lt;div class=&quot;highlight&quot; data-language=&quot;elixir&quot;&gt;
  &lt;pre class=&quot;language-elixir&quot;&gt;
    &lt;code class=&quot;language-elixir&quot;&gt;case :ets.lookup(:shop_auth_table, :user_id) do&lt;/code&gt;
    &lt;code class=&quot;language-elixir&quot;&gt;  [{_, user_id}] -&gt;&lt;/code&gt;
    &lt;code class=&quot;language-elixir&quot;&gt;    assign(conn, :user_id, user_id)&lt;/code&gt;
    &lt;code class=&quot;language-elixir&quot;&gt;&lt;/code&gt;
    &lt;code class=&quot;language-elixir&quot;&gt;  _ -&gt;&lt;/code&gt;
    &lt;code class=&quot;language-elixir&quot;&gt;    conn&lt;/code&gt;
    &lt;code class=&quot;language-elixir&quot;&gt;end&lt;/code&gt;
  &lt;/pre&gt;
&lt;/div&gt;

&lt;p&gt;Finally, I constructed another &lt;a href=&quot;https://github.com/toranb/cookie-authentication-phoenix-liveview-example/blob/0f7d2b58b6be7208039d8e40d2aa2eed041b7dfa/lib/shop_web/plugs/session.ex#L5-L16&quot;&gt;plug&lt;/a&gt; that would redirect the user if `user_id` was not found. This redirect plug only runs for restricted parts of the site, unlike the plug above which runs for every request.&lt;/p&gt;

&lt;div class=&quot;highlight&quot; data-language=&quot;elixir&quot;&gt;
  &lt;pre class=&quot;language-elixir&quot;&gt;
    &lt;code class=&quot;language-elixir&quot;&gt;def redirect_unauthorized(conn, _opts) do&lt;/code&gt;
    &lt;code class=&quot;language-elixir&quot;&gt;  user_id = Map.get(conn.assigns, :user_id)&lt;/code&gt;
    &lt;code class=&quot;language-elixir&quot;&gt;&lt;/code&gt;
    &lt;code class=&quot;language-elixir&quot;&gt;  if user_id == nil do&lt;/code&gt;
    &lt;code class=&quot;language-elixir&quot;&gt;    conn&lt;/code&gt;
    &lt;code class=&quot;language-elixir&quot;&gt;    |&gt; put_session(:return_to, conn.request_path)&lt;/code&gt;
    &lt;code class=&quot;language-elixir&quot;&gt;    |&gt; redirect(to: ShopWeb.Router.Helpers.login_path(conn, :index))&lt;/code&gt;
    &lt;code class=&quot;language-elixir&quot;&gt;    |&gt; halt()&lt;/code&gt;
    &lt;code class=&quot;language-elixir&quot;&gt;  else&lt;/code&gt;
    &lt;code class=&quot;language-elixir&quot;&gt;    conn&lt;/code&gt;
    &lt;code class=&quot;language-elixir&quot;&gt;  end&lt;/code&gt;
    &lt;code class=&quot;language-elixir&quot;&gt;end&lt;/code&gt;
  &lt;/pre&gt;
&lt;/div&gt;

&lt;blockquote&gt;Note: This solution isn&apos;t sound engineering for a few reasons but the most glaring can be seen after a single user id is inserted into ETS. After which any incoming request from any user will appear to be authenticated. While not production ready, this prototype did however validate a potential workaround that could be used to emulate `Plug.Conn.put_session`.&lt;/blockquote&gt;

&lt;h3&gt;LiveView Authentication&lt;/h3&gt;

&lt;p&gt;With this working prototype I now had a renewed sense of urgency to firm it up and share about what I&apos;d learned. To first step was to replace user id with a more privacy friendly value I refer to as `session_uuid` which should be self explanatory.&lt;/p&gt;

&lt;p&gt;On the &lt;a href=&quot;https://github.com/toranb/cookie-authentication-phoenix-liveview-example/blob/master/lib/shop_web/plugs/session.ex#L19-L27&quot;&gt;initial&lt;/a&gt; request a new `session_uuid` is generated and stored in the cookie with `Plug.Conn.put_session`. This unique session id will be used in the LiveView process to associate the user with a specific ETS entry at login. For any request after the first we simply defer to `ets.lookup` with the users `session_uuid` but more on that later.&lt;/p&gt;

&lt;div class=&quot;highlight&quot; data-language=&quot;elixir&quot;&gt;
  &lt;pre class=&quot;language-elixir&quot;&gt;
    &lt;code class=&quot;language-elixir&quot;&gt;case get_session(conn, :session_uuid) do&lt;/code&gt;
    &lt;code class=&quot;language-elixir&quot;&gt;  nil -&gt;&lt;/code&gt;
    &lt;code class=&quot;language-elixir&quot;&gt;    conn&lt;/code&gt;
    &lt;code class=&quot;language-elixir&quot;&gt;    |&gt; put_session(:session_uuid, Ecto.UUID.generate())&lt;/code&gt;
    &lt;code class=&quot;language-elixir&quot;&gt;&lt;/code&gt;
    &lt;code class=&quot;language-elixir&quot;&gt;  session_uuid -&gt;&lt;/code&gt;
    &lt;code class=&quot;language-elixir&quot;&gt;    conn&lt;/code&gt;
    &lt;code class=&quot;language-elixir&quot;&gt;    |&gt; validate_session_token(session_uuid)&lt;/code&gt;
    &lt;code class=&quot;language-elixir&quot;&gt;end&lt;/code&gt;
  &lt;/pre&gt;
&lt;/div&gt;

&lt;p&gt;In the LiveView for login I extract the `session_uuid` right away in &lt;a href=&quot;https://github.com/toranb/cookie-authentication-phoenix-liveview-example/blob/master/lib/shop_web/live/login_live.ex#L70-L76&quot;&gt;mount&lt;/a&gt; and set it with `assign` for use in the event handlers as needed.&lt;/p&gt;

&lt;div class=&quot;highlight&quot; data-language=&quot;elixir&quot;&gt;
  &lt;pre class=&quot;language-elixir&quot;&gt;
    &lt;code class=&quot;language-elixir&quot;&gt;def mount(_params, %{&quot;session_uuid&quot; =&gt; key}, socket) do&lt;/code&gt;
    &lt;code class=&quot;language-elixir&quot;&gt;  changeset = ...&lt;/code&gt;
    &lt;code class=&quot;language-elixir&quot;&gt;&lt;/code&gt;
    &lt;code class=&quot;language-elixir&quot;&gt;  {:ok, assign(socket, key: key, changeset: changeset)}&lt;/code&gt;
    &lt;code class=&quot;language-elixir&quot;&gt;end&lt;/code&gt;
  &lt;/pre&gt;
&lt;/div&gt;

&lt;p&gt;In the &lt;a href=&quot;https://github.com/toranb/cookie-authentication-phoenix-liveview-example/blob/master/lib/shop_web/live/login_live.ex#L121-L144&quot;&gt;event handler&lt;/a&gt; I pattern match out the `session_uuid`, &lt;a href=&quot;https://github.com/phoenixframework/phoenix/blob/e0e930cf7373f2b445c009f80a432d7de7de948c/lib/phoenix/token.ex#L110&quot;&gt;sign it&lt;/a&gt; to generate a token and finally I put that token into ETS. After all, this was the secret sauce of that original workaround but this time the user is more securely linked with help from the unique `session_uuid` value found in their cookie.&lt;/p&gt;

&lt;div class=&quot;highlight&quot; data-language=&quot;elixir&quot;&gt;
  &lt;pre class=&quot;language-elixir&quot;&gt;
    &lt;code class=&quot;language-elixir&quot;&gt;def handle_info({:disable_form, changeset}, %{assigns: %{:key =&gt; key}} = socket) do&lt;/code&gt;
    &lt;code class=&quot;language-elixir&quot;&gt;  case Login.authenticate_user(changeset) do&lt;/code&gt;
    &lt;code class=&quot;language-elixir&quot;&gt;    %Shop.User{id: user_id} -&gt;&lt;/code&gt;
    &lt;code class=&quot;language-elixir&quot;&gt;      salt = ShopWeb.Endpoint.config(:live_view)[:signing_salt]&lt;/code&gt;
    &lt;code class=&quot;language-elixir&quot;&gt;      token = Phoenix.Token.sign(ShopWeb.Endpoint, salt, user_id)&lt;/code&gt;
    &lt;code class=&quot;language-elixir&quot;&gt;      :ets.insert(:shop_auth_table, {:&quot;#{key}&quot;, token})&lt;/code&gt;
    &lt;code class=&quot;language-elixir&quot;&gt;&lt;/code&gt;
    &lt;code class=&quot;language-elixir&quot;&gt;      redirect = socket |&gt; redirect(to: some_path)&lt;/code&gt;
    &lt;code class=&quot;language-elixir&quot;&gt;      {:noreply, redirect}&lt;/code&gt;
    &lt;code class=&quot;language-elixir&quot;&gt;&lt;/code&gt;
    &lt;code class=&quot;language-elixir&quot;&gt;    changeset -&gt;&lt;/code&gt;
    &lt;code class=&quot;language-elixir&quot;&gt;      {:noreply, assign(socket, changeset: changeset)}&lt;/code&gt;
    &lt;code class=&quot;language-elixir&quot;&gt;  end&lt;/code&gt;
    &lt;code class=&quot;language-elixir&quot;&gt;end&lt;/code&gt;
  &lt;/pre&gt;
&lt;/div&gt;

&lt;p&gt;Looking back at the &lt;a href=&quot;https://github.com/toranb/cookie-authentication-phoenix-liveview-example/blob/master/lib/shop_web/plugs/session.ex#L30-L44&quot;&gt;plug code&lt;/a&gt; from earlier, when a user&apos;s `session_uuid` does yield a token I then verify it. When this token checks out I take the `user_id` and add it to `conn` for consistent use throughout the request life cycle just as I did in the server rendered plug.&lt;/p&gt;

&lt;blockquote&gt;Note: Below I use &lt;a href=&quot;https://github.com/phoenixframework/phoenix/blob/e0e930cf7373f2b445c009f80a432d7de7de948c/lib/phoenix/token.ex#L188&quot;&gt;Phoenix.Token.verify&lt;/a&gt; to extract the token and pull user id from it. In truth I started with this because I assumed it was somehow &quot;more secure&quot; but later that evolved into a practical mechanism to expire the session.&lt;/blockquote&gt;

&lt;div class=&quot;highlight&quot; data-language=&quot;elixir&quot;&gt;
  &lt;pre class=&quot;language-elixir&quot;&gt;
    &lt;code class=&quot;language-elixir&quot;&gt;def validate_session_token(conn, session_uuid) do&lt;/code&gt;
    &lt;code class=&quot;language-elixir&quot;&gt;  case :ets.lookup(:shop_auth_table, :&quot;#{session_uuid}&quot;) do&lt;/code&gt;
    &lt;code class=&quot;language-elixir&quot;&gt;    [{_, token}] -&gt;&lt;/code&gt;
    &lt;code class=&quot;language-elixir&quot;&gt;      case Phoenix.Token.verify(ShopWeb.Endpoint, signing_salt(), token, max_age: 806_400) do&lt;/code&gt;
    &lt;code class=&quot;language-elixir&quot;&gt;        {:ok, user_id} -&gt;&lt;/code&gt;
    &lt;code class=&quot;language-elixir&quot;&gt;          conn&lt;/code&gt;
    &lt;code class=&quot;language-elixir&quot;&gt;          |&gt; assign(:user_id, user_id)&lt;/code&gt;
    &lt;code class=&quot;language-elixir&quot;&gt;&lt;/code&gt;
    &lt;code class=&quot;language-elixir&quot;&gt;        _ -&gt;&lt;/code&gt;
    &lt;code class=&quot;language-elixir&quot;&gt;          conn&lt;/code&gt;
    &lt;code class=&quot;language-elixir&quot;&gt;&lt;/code&gt;
    &lt;code class=&quot;language-elixir&quot;&gt;      end&lt;/code&gt;
    &lt;code class=&quot;language-elixir&quot;&gt;  -&gt;&lt;/code&gt;
    &lt;code class=&quot;language-elixir&quot;&gt;    conn&lt;/code&gt;
    &lt;code class=&quot;language-elixir&quot;&gt;  end&lt;/code&gt;
    &lt;code class=&quot;language-elixir&quot;&gt;end&lt;/code&gt;
  &lt;/pre&gt;
&lt;/div&gt;

&lt;p&gt;Finally, I use the same `redirect_unauthorized` &lt;a href=&quot;https://github.com/toranb/cookie-authentication-phoenix-liveview-example/blob/master/lib/shop_web/plugs/session.ex#L5-L16&quot;&gt;plug&lt;/a&gt; to redirect the user if `user_id` isn&apos;t available in `conn.assigns`.&lt;/p&gt;

&lt;div class=&quot;highlight&quot; data-language=&quot;elixir&quot;&gt;
  &lt;pre class=&quot;language-elixir&quot;&gt;
    &lt;code class=&quot;language-elixir&quot;&gt;def redirect_unauthorized(conn, _opts) do&lt;/code&gt;
    &lt;code class=&quot;language-elixir&quot;&gt;  user_id = Map.get(conn.assigns, :user_id)&lt;/code&gt;
    &lt;code class=&quot;language-elixir&quot;&gt;&lt;/code&gt;
    &lt;code class=&quot;language-elixir&quot;&gt;  if user_id == nil do&lt;/code&gt;
    &lt;code class=&quot;language-elixir&quot;&gt;    conn&lt;/code&gt;
    &lt;code class=&quot;language-elixir&quot;&gt;    |&gt; put_session(:return_to, conn.request_path)&lt;/code&gt;
    &lt;code class=&quot;language-elixir&quot;&gt;    |&gt; redirect(to: ShopWeb.Router.Helpers.login_path(conn, :index))&lt;/code&gt;
    &lt;code class=&quot;language-elixir&quot;&gt;    |&gt; halt()&lt;/code&gt;
    &lt;code class=&quot;language-elixir&quot;&gt;  else&lt;/code&gt;
    &lt;code class=&quot;language-elixir&quot;&gt;    conn&lt;/code&gt;
    &lt;code class=&quot;language-elixir&quot;&gt;  end&lt;/code&gt;
    &lt;code class=&quot;language-elixir&quot;&gt;end&lt;/code&gt;
  &lt;/pre&gt;
&lt;/div&gt;

&lt;h3&gt;What about mount?&lt;/h3&gt;

&lt;p&gt;After I got login working, I flipped over to the restricted LiveView assuming I could get `user_id` without any trouble. Unfortunately I found you don&apos;t have access to `conn.assigns` from &lt;a href=&quot;https://github.com/toranb/cookie-authentication-phoenix-liveview-example/blob/master/lib/shop_web/live/shop_live.ex#L14-L24&quot;&gt;mount&lt;/a&gt; as I would have expected. To work around this constraint I used the `session_uuid` to get the token like I did in the &lt;a href=&quot;https://github.com/toranb/cookie-authentication-phoenix-liveview-example/blob/master/lib/shop_web/plugs/session.ex#L31-L36&quot;&gt;plug&lt;/a&gt;.&lt;/p&gt;

&lt;div class=&quot;highlight&quot; data-language=&quot;elixir&quot;&gt;
  &lt;pre class=&quot;language-elixir&quot;&gt;
    &lt;code class=&quot;language-elixir&quot;&gt;def mount(_params, session, socket) do&lt;/code&gt;
    &lt;code class=&quot;language-elixir&quot;&gt;  socket = assign_new(socket, :current_user, fn -&gt;&lt;/code&gt;
    &lt;code class=&quot;language-elixir&quot;&gt;    user_id = get_user_id(session)&lt;/code&gt;
    &lt;code class=&quot;language-elixir&quot;&gt;&lt;/code&gt;
    &lt;code class=&quot;language-elixir&quot;&gt;    Shop.User&lt;/code&gt;
    &lt;code class=&quot;language-elixir&quot;&gt;    |&gt; Shop.Repo.get(user_id)&lt;/code&gt;
    &lt;code class=&quot;language-elixir&quot;&gt;  end)&lt;/code&gt;
    &lt;code class=&quot;language-elixir&quot;&gt;&lt;/code&gt;
    &lt;code class=&quot;language-elixir&quot;&gt;  {:ok, socket}&lt;/code&gt;
    &lt;code class=&quot;language-elixir&quot;&gt;end&lt;/code&gt;
  &lt;/pre&gt;
&lt;/div&gt;

&lt;p&gt;The &lt;a href=&quot;https://github.com/toranb/cookie-authentication-phoenix-liveview-example/blob/master/lib/shop_web/live/shop_live.ex#L26-L40&quot;&gt;get_user_id&lt;/a&gt; function does exactly what the plug did to extract the token, then from it the `user_id` like you might expect.&lt;/p&gt;

&lt;div class=&quot;highlight&quot; data-language=&quot;elixir&quot;&gt;
  &lt;pre class=&quot;language-elixir&quot;&gt;
    &lt;code class=&quot;language-elixir&quot;&gt;def get_user_id(%{&quot;session_uuid&quot; =&gt; session_uuid}) do&lt;/code&gt;
    &lt;code class=&quot;language-elixir&quot;&gt;  case :ets.lookup(:shop_auth_table, :&quot;#{session_uuid}&quot;) do&lt;/code&gt;
    &lt;code class=&quot;language-elixir&quot;&gt;    [{_, token}] -&gt;&lt;/code&gt;
    &lt;code class=&quot;language-elixir&quot;&gt;      case Phoenix.Token.verify(ShopWeb.Endpoint, signing_salt(), token, max_age: 806_400) do&lt;/code&gt;
    &lt;code class=&quot;language-elixir&quot;&gt;        {:ok, user_id} -&gt;&lt;/code&gt;
    &lt;code class=&quot;language-elixir&quot;&gt;          user_id&lt;/code&gt;
    &lt;code class=&quot;language-elixir&quot;&gt;&lt;/code&gt;
    &lt;code class=&quot;language-elixir&quot;&gt;        _ -&gt;&lt;/code&gt;
    &lt;code class=&quot;language-elixir&quot;&gt;          nil&lt;/code&gt;
    &lt;code class=&quot;language-elixir&quot;&gt;      end&lt;/code&gt;
    &lt;code class=&quot;language-elixir&quot;&gt;&lt;/code&gt;
    &lt;code class=&quot;language-elixir&quot;&gt;    _ -&gt;&lt;/code&gt;
    &lt;code class=&quot;language-elixir&quot;&gt;      nil&lt;/code&gt;
    &lt;code class=&quot;language-elixir&quot;&gt;  end&lt;/code&gt;
    &lt;code class=&quot;language-elixir&quot;&gt;end&lt;/code&gt;
  &lt;/pre&gt;
&lt;/div&gt;

&lt;h3&gt;Bonus!&lt;/h3&gt;

&lt;p&gt;If you don&apos;t like the idea of running `:ets.lookup` along with `Phoenix.Token.verify` each time the mount function executes and you are cool with exposing `user_id` as part of the cookie you could instead alter the &lt;a href=&quot;https://github.com/toranb/cookie-authentication-phoenix-liveview-example/commit/90ff976085f1dc7197ca15f436169fcda5ade7bf&quot;&gt;plug&lt;/a&gt; that previously only set `conn.assigns` to also include a call to `put_session` with the `user_id`.&lt;/p&gt;

&lt;div class=&quot;highlight&quot; data-language=&quot;elixir&quot;&gt;
  &lt;pre class=&quot;language-elixir&quot;&gt;
    &lt;code class=&quot;language-elixir&quot;&gt;def validate_session_token(conn, session_uuid) do&lt;/code&gt;
    &lt;code class=&quot;language-elixir&quot;&gt;  case :ets.lookup(:shop_auth_table, :&quot;#{session_uuid}&quot;) do&lt;/code&gt;
    &lt;code class=&quot;language-elixir&quot;&gt;    [{_, token}] -&gt;&lt;/code&gt;
    &lt;code class=&quot;language-elixir&quot;&gt;      case Phoenix.Token.verify(ShopWeb.Endpoint, signing_salt(), token, max_age: 806_400) do&lt;/code&gt;
    &lt;code class=&quot;language-elixir&quot;&gt;        {:ok, user_id} -&gt;&lt;/code&gt;
    &lt;code class=&quot;language-elixir&quot;&gt;          conn&lt;/code&gt;
    &lt;code class=&quot;language-elixir&quot;&gt;          |&gt; assign(:user_id, user_id)&lt;/code&gt;
    &lt;code class=&quot;language-elixir&quot;&gt;          |&gt; put_session(&quot;user_id&quot;, user_id)&lt;/code&gt;
    &lt;code class=&quot;language-elixir&quot;&gt;&lt;/code&gt;
    &lt;code class=&quot;language-elixir&quot;&gt;        _ -&gt;&lt;/code&gt;
    &lt;code class=&quot;language-elixir&quot;&gt;          conn&lt;/code&gt;
    &lt;code class=&quot;language-elixir&quot;&gt;&lt;/code&gt;
    &lt;code class=&quot;language-elixir&quot;&gt;      end&lt;/code&gt;
    &lt;code class=&quot;language-elixir&quot;&gt;  -&gt;&lt;/code&gt;
    &lt;code class=&quot;language-elixir&quot;&gt;    conn&lt;/code&gt;
    &lt;code class=&quot;language-elixir&quot;&gt;  end&lt;/code&gt;
    &lt;code class=&quot;language-elixir&quot;&gt;end&lt;/code&gt;
  &lt;/pre&gt;
&lt;/div&gt;

&lt;p&gt;Now in the restricted LiveView I can get `user_id` in the &lt;a href=&quot;https://github.com/toranb/cookie-authentication-phoenix-liveview-example/commit/90ff976085f1dc7197ca15f436169fcda5ade7bf&quot;&gt;mount&lt;/a&gt; function with ease.&lt;/p&gt;

&lt;div class=&quot;highlight&quot; data-language=&quot;elixir&quot;&gt;
  &lt;pre class=&quot;language-elixir&quot;&gt;
    &lt;code class=&quot;language-elixir&quot;&gt;def mount(_params, %{&quot;user_id&quot; =&gt; user_id}, socket) do&lt;/code&gt;
    &lt;code class=&quot;language-elixir&quot;&gt;  socket = assign_new(socket, :current_user, fn -&gt;&lt;/code&gt;
    &lt;code class=&quot;language-elixir&quot;&gt;    Shop.User&lt;/code&gt;
    &lt;code class=&quot;language-elixir&quot;&gt;    |&gt; Shop.Repo.get(user_id)&lt;/code&gt;
    &lt;code class=&quot;language-elixir&quot;&gt;  end)&lt;/code&gt;
    &lt;code class=&quot;language-elixir&quot;&gt;&lt;/code&gt;
    &lt;code class=&quot;language-elixir&quot;&gt;  {:ok, socket}&lt;/code&gt;
    &lt;code class=&quot;language-elixir&quot;&gt;end&lt;/code&gt;
  &lt;/pre&gt;
&lt;/div&gt;

&lt;p&gt;You can find the source code from my adventure on &lt;a href=&quot;https://github.com/toranb/cookie-authentication-phoenix-liveview-example/commits/master&quot;&gt;github&lt;/a&gt;.&lt;/p&gt;
</description>
                <pubDate>Fri, 26 Jun 2020 18:00:00 +0000</pubDate>
                <link>http://toranbillups.com/blog/archive/2020/06/26/cookie-authentication-with-phoenix-liveview/</link>
                <guid isPermaLink="true">http://toranbillups.com/blog/archive/2020/06/26/cookie-authentication-with-phoenix-liveview/</guid>
            </item>
        
    </channel>
</rss>
