<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>Narain</title><link>https://blog.narain.dev/</link><description>Recent content on Narain</description><generator>Hugo -- gohugo.io</generator><language>en</language><lastBuildDate>Fri, 31 Oct 2025 17:15:52 +0000</lastBuildDate><atom:link href="https://blog.narain.dev/index.xml" rel="self" type="application/rss+xml"/><item><title>Synthetic Data Hackathon - AMD</title><link>https://blog.narain.dev/posts/synthetic-data-hackathon/</link><pubDate>Fri, 31 Oct 2025 17:15:52 +0000</pubDate><guid>https://blog.narain.dev/posts/synthetic-data-hackathon/</guid><description>&lt;h1 id="finalist-submission-methodology--technical-deep-dive">Finalist Submission Methodology &amp;amp; Technical Deep Dive&lt;/h1>
&lt;p>First and foremost, I’d like to extend my sincere gratitude to the AMD &amp;amp; Meta Hackathon organizers for orchestrating this intensive, high-impact competition especially for investing time in evaluating submissions, facilitating discussions, and reorganizing logistics to ensure fairness and transparency.&lt;/p>
&lt;p>I also want to thank all fellow competitors for their thoughtful contributions and active engagement throughout the event particularly those who participated in Discord threads, shared early insights, or offered peer feedback. Your collaboration elevated the entire experience.&lt;/p></description><content>&lt;h1 id="finalist-submission-methodology--technical-deep-dive">Finalist Submission Methodology &amp;amp; Technical Deep Dive&lt;/h1>
&lt;p>First and foremost, I’d like to extend my sincere gratitude to the AMD &amp;amp; Meta Hackathon organizers for orchestrating this intensive, high-impact competition especially for investing time in evaluating submissions, facilitating discussions, and reorganizing logistics to ensure fairness and transparency.&lt;/p>
&lt;p>I also want to thank all fellow competitors for their thoughtful contributions and active engagement throughout the event particularly those who participated in Discord threads, shared early insights, or offered peer feedback. Your collaboration elevated the entire experience.&lt;/p>
&lt;p>Special thanks to EDA Daniel and Sanyam for dedicating significant time to evaluate submissions, provide nuanced feedback, and help realign judging frameworks as needed.&lt;/p>
&lt;h2 id="problem-context-why-synthetic-data">Problem Context: Why Synthetic Data?&lt;/h2>
&lt;p>The core challenge of this hackathon centered around generating high-quality, domain-relevant synthetic datasets to train or fine-tune LLMs specifically for reasoning tasks involving character relationships and seating arrangement (linear and circular).&lt;/p>
&lt;p>Initial approaches involved Synthetic data generation involving a assorted set of aptitude like questions from &lt;a href="https://huggingface.co/datasets/nvidia/OpenMathReasoning">nvidia/OpenMathReasoning&lt;/a> .&lt;/p>
&lt;p>Early attempts at brute-force data generation using generic Aptitude datasets (including math, logic puzzles, and general knowledge Q&amp;amp;A) failed to produce usable training material because:&lt;/p>
&lt;ul>
&lt;li>The semantic relationships between entities were either absent or inconsistent.&lt;/li>
&lt;li>Outputs suffered from heavy repetition due to over-reliance on a single base model without diversification strategies.&lt;/li>
&lt;li>Generated samples lacked contextual grounding making them unsuitable for fine-tuning models intended for relational reasoning.&lt;/li>
&lt;/ul>
&lt;p>This led us to abandon “scrape-and-prompt” approaches and instead design a &lt;strong>multi-stage, relationship-aware synthetic data pipeline&lt;/strong> grounded in structured generation, guided prompting, and iterative refinement.&lt;/p>
&lt;h2 id="methodology-multi-stage-question-answer-pair-generation-pipeline">Methodology: Multi-Stage Question-Answer Pair Generation Pipeline&lt;/h2>
&lt;p>Prior to embarking on synthetic data generation, we dedicated substantial effort to curating a robust initial dataset. We recognized that a high-quality foundation maximizes training efficiency and long-term returns. Unsloth&amp;rsquo;s integration was pivotal throughout the hackathon, enabling seamless workflows without NaN errors—issues typically plaguing fp16 and quantized setups.&lt;/p>
&lt;p>To produce synthetic QA pairs encoding meaningful character and plot relationships—mirroring reasoning in story-based or logic tasks—we built a streamlined pipeline. We began by defining semantic relationships among characters, such as parent-child, grandparent-grandchild, niece-nephew, and in-laws. For instance, queries like “Who is the mother of the White Rabbit?” drew on inferred or fictional roles when not explicit, forming the core of our prompt engineering.&lt;/p>
&lt;p>Next, we assigned concrete names to these roles—e.g., Alice, White Rabbit, Cheshire Cat, Mad Hatter, March Hare—and framed questions around them for consistency. Prompts explicitly directed generation, e.g., “Create a multiple-choice question where the correct answer captures the relationship between [Character A] and [Character B], rooted in their story roles involving [Characters C, D, E].” This minimized hallucinations and boosted output alignment.&lt;/p>
&lt;p>Finally, we applied constrained LLM inference: first supplying both question and correct answer to elicit relationship-based explanations, then reversing to generate narrative-plausible distractors from the question and context alone. This bidirectional validation enhanced quality, diversity, and noise reduction. Again using structured output generation.&lt;/p>
&lt;h4 id="one-of-the-largely-found-exploit-in-arrangement-was-sorting">One of the largely found exploit in arrangement was sorting&lt;/h4>
&lt;blockquote>
&lt;p>Thanks to the tokenisation of Language models.&lt;/p>&lt;/blockquote>
&lt;p>A prominent vulnerability in seating arrangement tasks (linear or circular) involves alphabetical sorting of entity names. LLMs frequently err by defaulting to sorted orders when given random names, leading to incorrect inferences. We excluded such patterns from our training data to avoid propagating these flaws, as generated answers often replicated the errors.&lt;/p>
&lt;h2 id="technical-implementation-details">Technical Implementation details&lt;/h2>
&lt;h3 id="model-selection--training-setup">Model Selection &amp;amp; Training Setup&lt;/h3>
&lt;p>We experimented with several open-source LLMs known for strong reasoning capabilities:&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>&lt;/th>
&lt;th>&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>Llama 2 70B&lt;/td>
&lt;td>70B parameters&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Qwen3-Next 80B MoE&lt;/td>
&lt;td>80B MoE&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Phi-4&lt;/td>
&lt;td>Smaller, efficient&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>R1-based LLMs&lt;/td>
&lt;td>Custom fine-tuned variants&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>All models were run locally or via cloud APIs with controlled temperature settings (typically 0.3–0.7) to balance creativity vs. accuracy.&lt;/p>
&lt;h3 id="structured-output-grammar-enforcement">Structured Output (grammar) Enforcement&lt;/h3>
&lt;p>One critical breakthrough was enforcing &lt;strong>structured output formats&lt;/strong> during generation. Instead of free-form text, we required LLMs to return JSON-like structures containing:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-json" data-lang="json">&lt;span style="display:flex;">&lt;span>{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#f92672">&amp;#34;question&amp;#34;&lt;/span>: &lt;span style="color:#e6db74">&amp;#34;What is the relationship between Alice and the Cheshire Cat?&amp;#34;&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#f92672">&amp;#34;answer&amp;#34;&lt;/span>: &lt;span style="color:#e6db74">&amp;#34;Observer/Advisor&amp;#34;&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#f92672">&amp;#34;distractors&amp;#34;&lt;/span>: [&lt;span style="color:#e6db74">&amp;#34;Mother&amp;#34;&lt;/span>, &lt;span style="color:#e6db74">&amp;#34;Enemy&amp;#34;&lt;/span>, &lt;span style="color:#e6db74">&amp;#34;Student&amp;#34;&lt;/span>],
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#f92672">&amp;#34;relationship_type&amp;#34;&lt;/span>: &lt;span style="color:#e6db74">&amp;#34;narrative_role&amp;#34;&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#f92672">&amp;#34;context_hint&amp;#34;&lt;/span>: &lt;span style="color:#e6db74">&amp;#34;The Cheshire Cat provides cryptic advice but does not directly influence Alice&amp;#39;s actions.&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>This enabled automated parsing, deduplication, and downstream integration into training pipelines.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-json" data-lang="json">&lt;span style="display:flex;">&lt;span>{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">&amp;#34;topic&amp;#34;&lt;/span>: &lt;span style="color:#e6db74">&amp;#34;TopicEnum.FAMILY_TREE&amp;#34;&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">&amp;#34;question&amp;#34;&lt;/span>: &lt;span style="color:#e6db74">&amp;#34;Symbolic notation: A + B → A is mother of B; A – B → A is brother of B; A % B → A is father of B; A × B → A is sister of B. Which of the following shows that P is the maternal uncle of Q?, answer=C&amp;#34;&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">&amp;#34;reasoning&amp;#34;&lt;/span>: &lt;span style="color:#e6db74">&amp;#34;P – M → P is brother of M; M + N → M is mother of N; N × Q → N is sister of Q. So, P is brother of Q’s mother → maternal uncle.&amp;#34;&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">&amp;#34;options&amp;#34;&lt;/span>: [&lt;span style="color:#e6db74">&amp;#34;A. Q – N + M × P&amp;#34;&lt;/span>, &lt;span style="color:#e6db74">&amp;#34;B. P + S × N – Q&amp;#34;&lt;/span>, &lt;span style="color:#e6db74">&amp;#34;C. P – M + N × Q&amp;#34;&lt;/span>, &lt;span style="color:#e6db74">&amp;#34;D. Q – S % P&amp;#34;&lt;/span>]
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h1 id="question-generation-model">Question Generation model&lt;/h1>
&lt;p>// constraints&lt;/p>
&lt;blockquote>
&lt;p>max_token_limit = 1024
timelimit for generation = 10 seconds ~100 tokens/s&lt;/p>&lt;/blockquote>
&lt;h2 id="modeling-strategy">Modeling Strategy&lt;/h2>
&lt;h3 id="initial-training-phase-sft-with-system-prompts">Initial Training Phase: SFT with System Prompts&lt;/h3>
&lt;p>We began by fine-tuning base LLMs (Llama 3 70B, Qwen3-Next 80B MoE) using &lt;strong>system prompts similar to starter code&lt;/strong> but for faster iterations models like Qwen 4b was very benificial especially for RL finetuning.&lt;/p>
&lt;p>We also added &lt;strong>format enforcement instructions&lt;/strong> such as:&lt;/p>
&lt;blockquote>
&lt;p>“Always include exactly four options labeled A, B, C, D. Never omit any keys. Do not use markdown or plain text only valid JSON.”&lt;/p>&lt;/blockquote>
&lt;p>This reduced early-stage hallucinations and format violations.&lt;/p>
&lt;h3 id="grpo-reward-functions">GRPO Reward Functions&lt;/h3>
&lt;p>To overcome issues with output format and missing parts of the questions, we applied &lt;strong>Guided Reinforcement Policy Optimization (GRPO)&lt;/strong> with custom rewards:&lt;/p>
&lt;p>1.&lt;strong>JSON Format Compliance&lt;/strong>
2.&lt;strong>Presence of All Required Keys&lt;/strong> - Enforces inclusion of&lt;code>topic&lt;/code>,&lt;code>question&lt;/code>,&lt;code>reasoning&lt;/code>,&lt;code>options&lt;/code>
3.&lt;strong>Single-Character Answer Enum (A/B/C/D)&lt;/strong> -Prevents invalid answer labels like “Option C” or “C.”&lt;/p>
&lt;p>These rewards were weighted during reinforcement learning phases to nudge the model toward clean, machine-readable outputs.&lt;/p>
&lt;h3 id="what-didnt-work">What Didn’t Work&lt;/h3>
&lt;p>Despite initial success, we encountered critical issues while infering:&lt;/p>
&lt;h4 id="repetitive-outputs-due-to-static-system-prompts">Repetitive Outputs Due to Static System Prompts&lt;/h4>
&lt;p>Even after SFT, the model would generate near-identical questions repeatedly because the system prompt remained constant across generations.&lt;/p>
&lt;h4 id="flawed-questions-with-invalid-logic">Flawed Questions With Invalid Logic&lt;/h4>
&lt;p>Some generated questions had internal contradictions or unsolvable logic (e.g., “If A is brother of B and B is sister of C, then A is sister of C” false!).&lt;/p>
&lt;h4 id="unfinished-experimentation-cross-validation-via-answer-model">(Unfinished Experimentation): Cross-Validation via Answer Model&lt;/h4>
&lt;p>Started with the idea of exploring using the &lt;strong>trained Answer Generation Model&lt;/strong> as a &lt;strong>validator&lt;/strong>:&lt;/p>
&lt;blockquote>
&lt;p>For every generated question, we can pass it to the Answer Model and check validity:&lt;/p>
&lt;ul>
&lt;li>Does it return a valid JSON?&lt;/li>
&lt;li>Is the answer among A/B/C/D?&lt;/li>
&lt;li>Does the reasoning match the expected logic?&lt;/li>
&lt;/ul>&lt;/blockquote>
&lt;p>This could creaties a &lt;strong>self-correcting pipeline&lt;/strong> if the Answer Model couldn’t solve the question, the Question Model was penalized during GRPO updates.&lt;/p>
&lt;p>This could have significantly improved logical consistency and reduced flawed samples.&lt;/p>
&lt;h1 id="answer-generation-model">ANSWER GENERATION MODEL&lt;/h1>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-json" data-lang="json">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#75715e">// constraints
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#75715e">&lt;/span>{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#f92672">&amp;#34;max_token_limit&amp;#34;&lt;/span>: &lt;span style="color:#ae81ff">512&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#f92672">&amp;#34;time_limit&amp;#34;&lt;/span>: &lt;span style="color:#e6db74">&amp;#34;5 seconds (~100 tokens/sec)&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h2 id="modeling-strategy-1">Modeling Strategy&lt;/h2>
&lt;h3 id="intuition-behind-design">Intuition Behind Design&lt;/h3>
&lt;p>Rather than treating this as a simple “answer extraction” task, we modeled it as a &lt;strong>reasoning-first, answer-second&lt;/strong> process mimicking human problem-solving:&lt;/p>
&lt;blockquote>
&lt;p>“First understand the logic. Then deduce the answer. Only then present it clearly.”&lt;/p>&lt;/blockquote>
&lt;h3 id="supervised-fine-tuning-sft">Supervised Fine-Tuning (SFT)&lt;/h3>
&lt;p>Trained on curated &lt;strong>question-answer-reasoning triplets&lt;/strong> from our own dataset.&lt;/p>
&lt;p>Each sample followed this schema:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-json" data-lang="json">&lt;span style="display:flex;">&lt;span>{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#f92672">&amp;#34;question&amp;#34;&lt;/span>: &lt;span style="color:#e6db74">&amp;#34;...&amp;#34;&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#f92672">&amp;#34;reasoning&amp;#34;&lt;/span>: &lt;span style="color:#e6db74">&amp;#34;... step-by-step derivation ...&amp;#34;&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#f92672">&amp;#34;answer&amp;#34;&lt;/span>: &lt;span style="color:#e6db74">&amp;#34;C&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>We had two training phases here&lt;/p>
&lt;ol>
&lt;li>longer reasoning context&lt;/li>
&lt;li>Shorter reasoning context&lt;/li>
&lt;/ol>
&lt;p>The shorter reasoning helps us in reducing the token output.&lt;/p>
&lt;h2 id="grpo-reward-functions-1">GRPO Reward Functions&lt;/h2>
&lt;p>Applied during iterative tuning to refine behavior beyond basic accuracy:&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>&lt;/th>
&lt;th>&lt;/th>
&lt;th>&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>1.&lt;strong>Exact JSON Pattern Match&lt;/strong>&lt;/td>
&lt;td>1.0&lt;/td>
&lt;td>Must match predefined schema exactly&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>2.&lt;strong>Correct Answer Letter&lt;/strong>&lt;/td>
&lt;td>2.0&lt;/td>
&lt;td>Highest weight — getting the right choice matters most&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>3.&lt;strong>Approximate JSON Pattern&lt;/strong>&lt;/td>
&lt;td>0.5&lt;/td>
&lt;td>Tolerates minor formatting deviations if meaning preserved&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>4.&lt;strong>Presence of All Expected Keys&lt;/strong>&lt;/td>
&lt;td>0.1 per key&lt;/td>
&lt;td>Encourages completeness without over-penalizing&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>This weighting strategy prioritized &lt;strong>correctness &amp;gt; structure &amp;gt; completeness&lt;/strong>, which aligned with our goal: build a reliable validator for the Question Generator (TODO).&lt;/p>
&lt;h2 id="why-reasoning-matters-more-than-just-answer">Why Reasoning Matters More Than Just Answer&lt;/h2>
&lt;p>We found that &lt;strong>the reasoning field is crucial for labeling correctness&lt;/strong>.&lt;/p>
&lt;p>Example:&lt;/p>
&lt;blockquote>
&lt;p>Question: “Who is P’s maternal uncle?”&lt;br>
Option C: “P – M + N × Q”&lt;br>
Reasoning: “P – M → P is brother of M; M + N → M is mother of N; N × Q → N is sister of Q. So P is brother of Q’s mother → maternal uncle.”&lt;/p>&lt;/blockquote>
&lt;p>Without the reasoning, you can’t verify whether option C is truly correct — especially when symbolic notation varies.&lt;/p>
&lt;p>Thus, we treated reasoning as a &lt;strong>core component of ground truth&lt;/strong>, not just an explanation.&lt;/p>
&lt;h2 id="what-didnt-work-1">What Didn’t Work&lt;/h2>
&lt;h3 id="rewarding-low-token-usage-in-reasoning">Rewarding Low Token Usage in Reasoning&lt;/h3>
&lt;p>We initially tried rewarding shorter reasoning traces to encourage efficiency.&lt;/p>
&lt;p>Failed because:&lt;/p>
&lt;ul>
&lt;li>Often led to skipped steps or unjustified conclusions&lt;/li>
&lt;li>Reduced model confidence and increased error rates - removed this reward entirely. Instead, focused on &lt;strong>logical completeness&lt;/strong> and &lt;strong>clarity of inference chain&lt;/strong>.&lt;/li>
&lt;/ul>
&lt;h2 id="summary">Summary&lt;/h2>
&lt;p>For final submissions, the question model used a base, unfine-tuned Phi-4 14B, while the answering model employed Qwen3 14B with LoRA training via SFT and GRPO. To ensure compatibility and fairness, we adhered closely to the provided submission code templates, minimizing deviations.&lt;/p></content></item><item><title>Llm Inference on Lambda</title><link>https://blog.narain.dev/posts/llm-inference-on-lambda/</link><pubDate>Fri, 03 Jan 2025 01:01:19 -0700</pubDate><guid>https://blog.narain.dev/posts/llm-inference-on-lambda/</guid><description>&lt;p>when it comes to deploying machine learning models, there are varied options to choose from depending
on your scalability and cost requirements. A dedicated instance, for example, offers a stable environment
for serving models but often falls short in scalability, making it less ideal for workloads
with unpredictable traffic patterns. This is where a scalable distributed system like AWS Lambda.&lt;/p>
&lt;h3 id="what-does-lambda-provide">What does Lambda provide&lt;/h3>
&lt;p>AWS Lambda offers a serverless architecture that scales automatically with demand, ensuring you only
pay for the actual computation times used. For lightweight and quantized machine learning models.
especially those finetuned for specific tasks, Lambda provides an efficient deployment option. with its
support for up to 6 vCPUs and 10 GB of memory, it can handle smaller models effectively, sufficient to
run some mobile YOLO models and llm models which are optimized for inference using GGML.&lt;/p></description><content>&lt;p>when it comes to deploying machine learning models, there are varied options to choose from depending
on your scalability and cost requirements. A dedicated instance, for example, offers a stable environment
for serving models but often falls short in scalability, making it less ideal for workloads
with unpredictable traffic patterns. This is where a scalable distributed system like AWS Lambda.&lt;/p>
&lt;h3 id="what-does-lambda-provide">What does Lambda provide&lt;/h3>
&lt;p>AWS Lambda offers a serverless architecture that scales automatically with demand, ensuring you only
pay for the actual computation times used. For lightweight and quantized machine learning models.
especially those finetuned for specific tasks, Lambda provides an efficient deployment option. with its
support for up to 6 vCPUs and 10 GB of memory, it can handle smaller models effectively, sufficient to
run some mobile YOLO models and llm models which are optimized for inference using GGML.&lt;/p>
&lt;p>However deploying complex models like llama-cpp in a virtualized Lambda environment comes with unique
challeges.&lt;/p>
&lt;ul>
&lt;li>Restrictions on specialized CPU instructions (such as AVX512 and AMX) which are available on some of
the servers that AWS provides but are not available for use.&lt;/li>
&lt;li>If some lambda function is idle it will go to cold start state and the next time it is invoked it
requires a startup time to allocate a machine and start up the server.&lt;/li>
&lt;/ul>
&lt;p>running machine learning models which are compute demanding require some careful configuration.&lt;/p>
&lt;p>below i have provided some basic configuration of running llama cpp on lambda on aws. with this setup
you should be good enough to run nearly all machine learning models on lambda once it is converted to gguf
format&lt;/p>
&lt;div class="collapsable-code">
&lt;input id="1" type="checkbox" checked />
&lt;label for="1">
&lt;span class="collapsable-code__language">python&lt;/span>
&lt;span class="collapsable-code__title">inference script&lt;/span>
&lt;span class="collapsable-code__toggle" data-label-expand="Show" data-label-collapse="Hide">&lt;/span>
&lt;/label>
&lt;pre class="language-python" >&lt;code>
from llama_cpp import Llama
def download_model_to_tmp():
pass
def initialize_model():
if model_exists_in_tmp(): download_model_to_tmp()
llm = Llama(
model_path=&amp;#34;/tmp/lama-model.gguf&amp;#34;,
)
return True
def lambda_handler(event, context):
if event[&amp;#39;body&amp;#39;][&amp;#39;health_check&amp;#39;]: return initialize_model()
response = llm(
&amp;#34;Q: Name the planets in the solar system? A: &amp;#34;, # Prompt
max_tokens=32,
stop=[&amp;#34;Q:&amp;#34;, &amp;#34;\n&amp;#34;],
echo=True
)
return response
&lt;/code>&lt;/pre>
&lt;/div>
&lt;h4 id="code-of-the-dockerfile-which-is-required-for-the-build">Code of the dockerfile which is required for the build&lt;/h4>
&lt;pre tabindex="0">&lt;code>FROM --platform=linux/amd64 python:3.11-slim as build-image
ARG FUNCTION_DIR=&amp;#34;/function&amp;#34;
RUN mkdir -p ${FUNCTION_DIR}
WORKDIR ${FUNCTION_DIR}
COPY inference.py .
RUN apt-get update \
&amp;amp;&amp;amp; apt-get install -y --no-install-recommends \
build-essential \
cmake \
libopenblas-dev \
libgomp1 \
pkg-config \
&amp;amp;&amp;amp; rm -rf /var/lib/apt/lists/*
RUN CMAKE_ARGS=&amp;#34;-DGGML_BLAS=ON -DGGML_BLAS_VENDOR=OpenBLAS \
-DCMAKE_CXX_FLAGS=\&amp;#34;-march=x86-64\&amp;#34; -DLLAMA_AVX512=OFF \
-DLLAMA_AVX2=OFF -DLLAMA_AVX=OFF -DLLAMA_FMA=OFF \
-DLLAMA_F16C=OFF -DLLAMA_BUILD_SERVER=1 \
-DLLAMA_CUBLAS=OFF -DGGML_NATIVE=OFF&amp;#34; \
pip install --target ${FUNCTION_DIR} llama-cpp-python
RUN pip install --target ${FUNCTION_DIR} --no-cache-dir boto3
RUN pip install --target ${FUNCTION_DIR} --no-cache-dir awslambdaric==2.0.7
COPY --from=public.ecr.aws/lambda/python:3.11 /var/runtime /var/runtime
COPY --from=public.ecr.aws/lambda/python:3.11 /var/lang /var/lang
COPY --from=public.ecr.aws/lambda/python:3.11 /usr/lib64 /usr/lib64
COPY --from=public.ecr.aws/lambda/python:3.11 /opt /opt
FROM --platform=linux/amd64 python:3.11-slim as runtime-image
ARG FUNCTION_DIR=&amp;#34;/function&amp;#34;
WORKDIR ${FUNCTION_DIR}
RUN apt-get update \
&amp;amp;&amp;amp; apt-get -y install libopenblas-dev libgomp1 \
&amp;amp;&amp;amp; rm -rf /var/lib/apt/lists/*
COPY --from=build-image ${FUNCTION_DIR} ${FUNCTION_DIR}
COPY --from=public.ecr.aws/lambda/python:3.11 /var/runtime /var/runtime
ENTRYPOINT [ &amp;#34;/usr/local/bin/python&amp;#34;, &amp;#34;-m&amp;#34;, &amp;#34;awslambdaric&amp;#34; ]
CMD [ &amp;#34;inference.lambda_handler&amp;#34; ]
&lt;/code>&lt;/pre>&lt;h1 id="references">References&lt;/h1>
&lt;ul>
&lt;li>&lt;a href="https://github.com/abetlen/llama-cpp-python">llama cpp python&lt;/a>&lt;/li>
&lt;li>&lt;a href="https://github.com/ggerganov/llama.cpp">llama cpp&lt;/a>&lt;/li>
&lt;/ul></content></item><item><title>OMP parallelization</title><link>https://blog.narain.dev/posts/omp_parallelization/</link><pubDate>Wed, 20 Nov 2024 01:15:54 -0700</pubDate><guid>https://blog.narain.dev/posts/omp_parallelization/</guid><description>&lt;p>OpenMP (Open Multi-Processing) is an High level API for parallel programming in C, C++, and Fortran.
It&amp;rsquo;s a widely-used, open-standard, and platform-independent library that allows developers to
write parallel code that can run on multiple CPU cores, reducing the overall execution time of a program.&lt;/p>
&lt;p>OpenMP does the heavy lifting in several areas:&lt;/p>
&lt;ol>
&lt;li>&lt;strong>Thread Management&lt;/strong>: OpenMP automatically manages thread creation, synchronization, and termination, freeing the developer from the complexity of thread management.&lt;/li>
&lt;li>&lt;strong>Work Distribution&lt;/strong>: OpenMP provides a way to distribute work among threads, ensuring that each thread has a portion of the work to execute.&lt;/li>
&lt;li>&lt;strong>Synchronization&lt;/strong>: OpenMP provides built-in synchronization mechanisms, such as barriers and locks, to ensure that threads access shared data safely.&lt;/li>
&lt;li>&lt;strong>Data Management&lt;/strong>: OpenMP provides a way to manage data sharing between threads, reducing the risk of data corruption and inconsistencies.&lt;/li>
&lt;/ol>
&lt;p>OpenMP makes it easy to experiment with parallelism by:&lt;/p></description><content>&lt;p>OpenMP (Open Multi-Processing) is an High level API for parallel programming in C, C++, and Fortran.
It&amp;rsquo;s a widely-used, open-standard, and platform-independent library that allows developers to
write parallel code that can run on multiple CPU cores, reducing the overall execution time of a program.&lt;/p>
&lt;p>OpenMP does the heavy lifting in several areas:&lt;/p>
&lt;ol>
&lt;li>&lt;strong>Thread Management&lt;/strong>: OpenMP automatically manages thread creation, synchronization, and termination, freeing the developer from the complexity of thread management.&lt;/li>
&lt;li>&lt;strong>Work Distribution&lt;/strong>: OpenMP provides a way to distribute work among threads, ensuring that each thread has a portion of the work to execute.&lt;/li>
&lt;li>&lt;strong>Synchronization&lt;/strong>: OpenMP provides built-in synchronization mechanisms, such as barriers and locks, to ensure that threads access shared data safely.&lt;/li>
&lt;li>&lt;strong>Data Management&lt;/strong>: OpenMP provides a way to manage data sharing between threads, reducing the risk of data corruption and inconsistencies.&lt;/li>
&lt;/ol>
&lt;p>OpenMP makes it easy to experiment with parallelism by:&lt;/p>
&lt;ol>
&lt;li>&lt;strong>Incremental Parallelism&lt;/strong>: OpenMP allows developers to incrementally add parallelism to existing serial code, making it easier to test and refine parallel code.&lt;/li>
&lt;li>&lt;strong>Simple Directives&lt;/strong>: OpenMP uses simple directives (e.g., &lt;code>#pragma omp parallel&lt;/code>) to indicate parallel regions, making it easy to write parallel code.&lt;/li>
&lt;li>&lt;strong>Portability&lt;/strong>: OpenMP code is portable across different platforms, including Windows, macOS, and Linux.&lt;/li>
&lt;/ol>
&lt;p>&lt;strong>Comparison with POSIX Threads (pthreads)&lt;/strong>&lt;/p>
&lt;p>POSIX threads (pthreads) is a low-level, Unix-based threading API that provides a way to create and manage threads. Here&amp;rsquo;s a comparison with OpenMP:&lt;/p>
&lt;p>Note: this blog stands as a code first introduction of the directives and clauses that openmp provides
most of the time you can get away with just using openmp for loop parallelization but when you want to
get a little deeper into scheduling and having private and reduction variables this would stand as
a all in place reference.&lt;/p>
&lt;h3 id="openmp-directives">OpenMP Directives&lt;/h3>
&lt;ul>
&lt;li>&lt;a href="https://blog.narain.dev/posts/omp_parallelization/#parallel-directive">Parallel Directive&lt;/a>&lt;/li>
&lt;li>&lt;a href="https://blog.narain.dev/posts/omp_parallelization/#sections-directive">Sections Directive&lt;/a>&lt;/li>
&lt;li>&lt;a href="https://blog.narain.dev/posts/omp_parallelization/#single-directive">Single Directive&lt;/a>&lt;/li>
&lt;li>&lt;a href="https://blog.narain.dev/posts/omp_parallelization/#master-directive">Master Directive&lt;/a>&lt;/li>
&lt;li>&lt;a href="https://blog.narain.dev/posts/omp_parallelization/#critical-directive">Critical Directive&lt;/a>&lt;/li>
&lt;li>&lt;a href="https://blog.narain.dev/posts/omp_parallelization/#barrier-directive">Barrier Directive&lt;/a>&lt;/li>
&lt;/ul>
&lt;h3 id="openmp-clauses">OpenMP Clauses&lt;/h3>
&lt;ul>
&lt;li>&lt;a href="https://blog.narain.dev/posts/omp_parallelization/#private-clause">Private Clause&lt;/a>&lt;/li>
&lt;li>&lt;a href="https://blog.narain.dev/posts/omp_parallelization/#firstprivate-clause">Firstprivate Clause&lt;/a>&lt;/li>
&lt;li>&lt;a href="https://blog.narain.dev/posts/omp_parallelization/#lastprivate-clause">Lastprivate Clause&lt;/a>&lt;/li>
&lt;li>&lt;a href="https://blog.narain.dev/posts/omp_parallelization/#reduction-clause">Reduction Clause&lt;/a>&lt;/li>
&lt;li>&lt;a href="https://blog.narain.dev/posts/omp_parallelization/#schedule-clause">Schedule Clause&lt;/a>
&lt;ul>
&lt;li>&lt;a href="https://blog.narain.dev/posts/omp_parallelization/#static-schedule">Static Schedule&lt;/a>&lt;/li>
&lt;li>&lt;a href="https://blog.narain.dev/posts/omp_parallelization/#dynamic-schedule">Dynamic Schedule&lt;/a>&lt;/li>
&lt;li>&lt;a href="https://blog.narain.dev/posts/omp_parallelization/#guided-schedule">Guided Schedule&lt;/a>&lt;/li>
&lt;li>&lt;a href="https://blog.narain.dev/posts/omp_parallelization/#auto-schedule">Auto Schedule&lt;/a>&lt;/li>
&lt;li>&lt;a href="https://blog.narain.dev/posts/omp_parallelization/#runtime-schedule">Runtime Schedule&lt;/a>&lt;/li>
&lt;/ul>
&lt;/li>
&lt;/ul>
&lt;h3 id="parallel-directive">&lt;strong>Parallel Directive&lt;/strong>&lt;/h3>
&lt;p>The &lt;code>parallel&lt;/code> directive is used to specify a block of code that should be executed in parallel by multiple threads. This directive is the foundation of OpenMP programming, and it is used to create a team of threads that will execute the code within the parallel region.&lt;/p>
&lt;div class="collapsable-code">
&lt;input id="1" type="checkbox" checked />
&lt;label for="1">
&lt;span class="collapsable-code__language">c&lt;/span>
&lt;span class="collapsable-code__title">Parallel Directive&lt;/span>
&lt;span class="collapsable-code__toggle" data-label-expand="Show" data-label-collapse="Hide">&lt;/span>
&lt;/label>
&lt;pre class="language-c" >&lt;code>
#include &amp;lt;omp.h&amp;gt;
#include &amp;lt;stdio.h&amp;gt;
int main() {
#pragma omp parallel
{
printf(&amp;#34;Hello from thread %d\n&amp;#34;, omp_get_thread_num());
}
return 0;
}
&lt;/code>&lt;/pre>
&lt;/div>
&lt;p>&lt;strong>Note:&lt;/strong> The &lt;code>omp_get_thread_num()&lt;/code> function is used to get the thread number of the current thread.&lt;/p>
&lt;h3 id="sections-directive">&lt;strong>Sections Directive&lt;/strong>&lt;/h3>
&lt;p>The &lt;code>sections&lt;/code> directive is used to divide a block of code into multiple sections that can be executed in parallel by different threads. This directive is useful when you have multiple independent tasks that can be executed concurrently.&lt;/p>
&lt;div class="collapsable-code">
&lt;input id="2" type="checkbox" checked />
&lt;label for="2">
&lt;span class="collapsable-code__language">c&lt;/span>
&lt;span class="collapsable-code__title">Sections Directive&lt;/span>
&lt;span class="collapsable-code__toggle" data-label-expand="Show" data-label-collapse="Hide">&lt;/span>
&lt;/label>
&lt;pre class="language-c" >&lt;code>
#include &amp;lt;omp.h&amp;gt;
#include &amp;lt;stdio.h&amp;gt;
int main() {
#pragma omp parallel sections
{
#pragma omp section
{
printf(&amp;#34;Section 1: Hello from thread %d\n&amp;#34;, omp_get_thread_num());
}
#pragma omp section
{
printf(&amp;#34;Section 2: Hello from thread %d\n&amp;#34;, omp_get_thread_num());
}
}
return 0;
}
&lt;/code>&lt;/pre>
&lt;/div>
&lt;h3 id="single-directive">&lt;strong>Single Directive&lt;/strong>&lt;/h3>
&lt;p>The Single Directive is used to specify a block of code that should be executed by only one thread in a team. This directive is useful when you want to perform some operation that should only be done once, such as initializing a shared variable or printing a message.&lt;/p>
&lt;blockquote>
&lt;p>#pragma omp single
{
// code to be executed by a single thread
}&lt;/p>&lt;/blockquote>
&lt;p>&lt;strong>Example&lt;/strong>&lt;/p>
&lt;div class="collapsable-code">
&lt;input id="4" type="checkbox" checked />
&lt;label for="4">
&lt;span class="collapsable-code__language">c&lt;/span>
&lt;span class="collapsable-code__title">Single Directive&lt;/span>
&lt;span class="collapsable-code__toggle" data-label-expand="Show" data-label-collapse="Hide">&lt;/span>
&lt;/label>
&lt;pre class="language-c" >&lt;code>
#include &amp;lt;omp.h&amp;gt;
#include &amp;lt;stdio.h&amp;gt;
int main() {
int x = 0;
#pragma omp parallel
{
#pragma omp single
{
x = 10;
printf(&amp;#34;Thread %d initialized x to %d\n&amp;#34;, omp_get_thread_num(), x);
}
printf(&amp;#34;Thread %d sees x as %d\n&amp;#34;, omp_get_thread_num(), x);
}
return 0;
}
&lt;/code>&lt;/pre>
&lt;/div>
&lt;p>&lt;strong>Output&lt;/strong>&lt;/p>
&lt;pre tabindex="0">&lt;code>Thread 0 initialized x to 10
Thread 0 sees x as 10
Thread 1 sees x as 10
Thread 2 sees x as 10
Thread 3 sees x as 10
&lt;/code>&lt;/pre>&lt;p>As you can see, only one thread (Thread 0) initializes the variable &lt;code>x&lt;/code> to 10, and all other threads see the updated value of &lt;code>x&lt;/code>.&lt;/p>
&lt;p>&lt;strong>Note&lt;/strong>&lt;/p>
&lt;p>The Single Directive does not imply a barrier, so the other threads may continue executing without waiting for the single thread to finish its execution. If you need to ensure that all threads wait for the single thread to finish, you can use the &lt;code>#pragma omp barrier&lt;/code> directive after the Single Directive.&lt;/p>
&lt;p>Here&amp;rsquo;s an updated example with a barrier:&lt;/p>
&lt;div class="collapsable-code">
&lt;input id="5" type="checkbox" checked />
&lt;label for="5">
&lt;span class="collapsable-code__language">c&lt;/span>
&lt;span class="collapsable-code__title">Single Directive&lt;/span>
&lt;span class="collapsable-code__toggle" data-label-expand="Show" data-label-collapse="Hide">&lt;/span>
&lt;/label>
&lt;pre class="language-c" >&lt;code>
#include &amp;lt;omp.h&amp;gt;
#include &amp;lt;stdio.h&amp;gt;
int main() {
int x = 0;
#pragma omp parallel
{
#pragma omp single
{
x = 10;
printf(&amp;#34;Thread %d initialized x to %d\n&amp;#34;, omp_get_thread_num(), x);
}
#pragma omp barrier
printf(&amp;#34;Thread %d sees x as %d\n&amp;#34;, omp_get_thread_num(), x);
}
return 0;
}
&lt;/code>&lt;/pre>
&lt;/div>
&lt;p>This ensures that all threads wait for the single thread to finish initializing &lt;code>x&lt;/code> before they continue executing.&lt;/p>
&lt;h3 id="master-directive">&lt;strong>Master Directive&lt;/strong>&lt;/h3>
&lt;p>The &lt;code>master&lt;/code> directive is used to specify a block of code that should be executed by the master thread only. The master thread is the thread that has a thread number of 0.&lt;/p>
&lt;p>&lt;strong>Syntax&lt;/strong>&lt;/p>
&lt;blockquote>
&lt;p>#pragma omp master
{
// code to be executed by the master thread
}&lt;/p>&lt;/blockquote>
&lt;p>&lt;strong>Example&lt;/strong>&lt;/p>
&lt;div class="collapsable-code">
&lt;input id="6" type="checkbox" checked />
&lt;label for="6">
&lt;span class="collapsable-code__language">c&lt;/span>
&lt;span class="collapsable-code__title">Master Directive&lt;/span>
&lt;span class="collapsable-code__toggle" data-label-expand="Show" data-label-collapse="Hide">&lt;/span>
&lt;/label>
&lt;pre class="language-c" >&lt;code>
#include &amp;lt;omp.h&amp;gt;
#include &amp;lt;stdio.h&amp;gt;
int main() {
int x = 0;
#pragma omp parallel
{
#pragma omp master
{
x = 10;
printf(&amp;#34;Master thread: x = %d\n&amp;#34;, x);
}
printf(&amp;#34;Thread %d: x = %d\n&amp;#34;, omp_get_thread_num(), x);
}
return 0;
}
&lt;/code>&lt;/pre>
&lt;/div>
&lt;p>In this example, the master thread sets the value of &lt;code>x&lt;/code> to 10 and prints a message. All threads then print the value of &lt;code>x&lt;/code>.&lt;/p>
&lt;p>&lt;strong>Note&lt;/strong>&lt;/p>
&lt;p>The &lt;code>master&lt;/code> directive does not imply a barrier, so the other threads may continue executing without waiting for the master thread to finish its execution.&lt;/p>
&lt;p>&lt;strong>When to use&lt;/strong>&lt;/p>
&lt;p>The &lt;code>master&lt;/code> directive is useful when you need to perform some operation that should only be done once, such as initializing a shared variable or printing a message.&lt;/p>
&lt;p>&lt;strong>Comparison with Single Directive&lt;/strong>&lt;/p>
&lt;p>The &lt;code>master&lt;/code> directive is similar to the &lt;code>single&lt;/code> directive, but the &lt;code>single&lt;/code> directive can be executed by any thread, while the &lt;code>master&lt;/code> directive is always executed by the master thread.&lt;/p>
&lt;p>Here is an example that demonstrates the difference:&lt;/p>
&lt;div class="collapsable-code">
&lt;input id="7" type="checkbox" checked />
&lt;label for="7">
&lt;span class="collapsable-code__language">c&lt;/span>
&lt;span class="collapsable-code__title">Master Directive&lt;/span>
&lt;span class="collapsable-code__toggle" data-label-expand="Show" data-label-collapse="Hide">&lt;/span>
&lt;/label>
&lt;pre class="language-c" >&lt;code>
#include &amp;lt;omp.h&amp;gt;
#include &amp;lt;stdio.h&amp;gt;
int main() {
int x = 0;
#pragma omp parallel
{
#pragma omp single
{
x = 10;
printf(&amp;#34;Single thread: x = %d\n&amp;#34;, x);
}
printf(&amp;#34;Thread %d: x = %d\n&amp;#34;, omp_get_thread_num(), x);
}
return 0;
}
&lt;/code>&lt;/pre>
&lt;/div>
&lt;p>In this example, the &lt;code>single&lt;/code> directive is used to set the value of &lt;code>x&lt;/code> to 10. The thread that executes the &lt;code>single&lt;/code> directive may not be the master thread.&lt;/p>
&lt;h3 id="critical-directive">&lt;strong>Critical Directive&lt;/strong>&lt;/h3>
&lt;p>The &lt;code>critical&lt;/code> directive is used to specify a block of code that should be executed by only one thread at a time. This directive is useful when you need to protect a critical section of code that should not be executed concurrently by multiple threads.&lt;/p>
&lt;p>&lt;strong>Syntax&lt;/strong>&lt;/p>
&lt;blockquote>
&lt;p>#pragma omp critical [(name)]
{
// code to be executed by only one thread at a time
}&lt;/p>&lt;/blockquote>
&lt;p>&lt;strong>Example&lt;/strong>&lt;/p>
&lt;div class="collapsable-code">
&lt;input id="8" type="checkbox" checked />
&lt;label for="8">
&lt;span class="collapsable-code__language">c&lt;/span>
&lt;span class="collapsable-code__title">Critical Directive&lt;/span>
&lt;span class="collapsable-code__toggle" data-label-expand="Show" data-label-collapse="Hide">&lt;/span>
&lt;/label>
&lt;pre class="language-c" >&lt;code>
#include &amp;lt;omp.h&amp;gt;
#include &amp;lt;stdio.h&amp;gt;
int main() {
int x = 0;
#pragma omp parallel
{
#pragma omp critical
{
x = x &amp;#43; 1;
printf(&amp;#34;Thread %d: x = %d\n&amp;#34;, omp_get_thread_num(), x);
}
}
return 0;
}
&lt;/code>&lt;/pre>
&lt;/div>
&lt;p>In this example, the &lt;code>critical&lt;/code> directive is used to protect the increment of the variable &lt;code>x&lt;/code>. Only one thread can execute the critical section at a time, ensuring that the increment is atomic.&lt;/p>
&lt;p>&lt;strong>Named Critical Sections&lt;/strong>&lt;/p>
&lt;p>You can also specify a name for the critical section, which allows you to have multiple critical sections that can be executed concurrently.&lt;/p>
&lt;blockquote>
&lt;p>#pragma omp critical (name)
{
// code to be executed by only one thread at a time
}&lt;/p>&lt;/blockquote>
&lt;p>&lt;strong>Example&lt;/strong>&lt;/p>
&lt;div class="collapsable-code">
&lt;input id="8" type="checkbox" checked />
&lt;label for="8">
&lt;span class="collapsable-code__language">c&lt;/span>
&lt;span class="collapsable-code__title">Critical Directive&lt;/span>
&lt;span class="collapsable-code__toggle" data-label-expand="Show" data-label-collapse="Hide">&lt;/span>
&lt;/label>
&lt;pre class="language-c" >&lt;code>
#include &amp;lt;omp.h&amp;gt;
#include &amp;lt;stdio.h&amp;gt;
int main() {
int x = 0;
int y = 0;
#pragma omp parallel
{
#pragma omp critical (x_lock)
{
x = x &amp;#43; 1;
printf(&amp;#34;Thread %d: x = %d\n&amp;#34;, omp_get_thread_num(), x);
}
#pragma omp critical (y_lock)
{
y = y &amp;#43; 1;
printf(&amp;#34;Thread %d: y = %d\n&amp;#34;, omp_get_thread_num(), y);
}
}
return 0;
}
&lt;/code>&lt;/pre>
&lt;/div>
&lt;p>In this example, we have two named critical sections, &lt;code>x_lock&lt;/code> and &lt;code>y_lock&lt;/code>. These critical sections can be executed concurrently, but only one thread can execute each critical section at a time.&lt;/p>
&lt;p>&lt;strong>Best Practices&lt;/strong>&lt;/p>
&lt;ul>
&lt;li>Use the &lt;code>critical&lt;/code> directive to protect critical sections of code that should not be executed concurrently by multiple threads.&lt;/li>
&lt;li>Use named critical sections to allow multiple critical sections to be executed concurrently.&lt;/li>
&lt;li>Minimize the use of critical sections, as they can introduce performance overhead.&lt;/li>
&lt;li>Use other synchronization mechanisms, such as locks or atomic operations, when possible.&lt;/li>
&lt;/ul>
&lt;p>&lt;strong>Barrier Directive&lt;/strong>&lt;/p>
&lt;p>The &lt;code>barrier&lt;/code> directive is used to synchronize all threads in a team. When a thread reaches a barrier, it waits until all other threads in the team have also reached the barrier. Once all threads have reached the barrier, they can all proceed past it.&lt;/p>
&lt;p>&lt;strong>Syntax&lt;/strong>&lt;/p>
&lt;blockquote>
&lt;p>#pragma omp barrier&lt;/p>&lt;/blockquote>
&lt;p>&lt;strong>Example&lt;/strong>&lt;/p>
&lt;p>Here&amp;rsquo;s an example demonstrating the use of the barrier directive:&lt;/p>
&lt;div class="collapsable-code">
&lt;input id="9" type="checkbox" checked />
&lt;label for="9">
&lt;span class="collapsable-code__language">c&lt;/span>
&lt;span class="collapsable-code__title">Barrier Directive&lt;/span>
&lt;span class="collapsable-code__toggle" data-label-expand="Show" data-label-collapse="Hide">&lt;/span>
&lt;/label>
&lt;pre class="language-c" >&lt;code>
#include &amp;lt;omp.h&amp;gt;
#include &amp;lt;stdio.h&amp;gt;
#include &amp;lt;unistd.h&amp;gt;
int main() {
#pragma omp parallel num_threads(4)
{
int thread_num = omp_get_thread_num();
// First part of the work
printf(&amp;#34;Thread %d: Starting first part of work\n&amp;#34;, thread_num);
sleep(thread_num); // Simulate different work times
#pragma omp barrier
// Second part of the work
printf(&amp;#34;Thread %d: Starting second part of work\n&amp;#34;, thread_num);
}
return 0;
}
&lt;/code>&lt;/pre>
&lt;/div>
&lt;p>&lt;strong>Output&lt;/strong>&lt;/p>
&lt;pre tabindex="0">&lt;code>Thread 0: Starting first part of work
Thread 3: Starting first part of work
Thread 2: Starting first part of work
Thread 1: Starting first part of work
Thread 0: Starting second part of work
Thread 1: Starting second part of work
Thread 2: Starting second part of work
Thread 3: Starting second part of work
&lt;/code>&lt;/pre>&lt;p>&lt;strong>Explanation&lt;/strong>&lt;/p>
&lt;ol>
&lt;li>Four threads are created.&lt;/li>
&lt;li>Each thread prints a message and then sleeps for a duration equal to its thread number (simulating different work times).&lt;/li>
&lt;li>The &lt;code>barrier&lt;/code> directive ensures that all threads wait at this point until every thread has reached the barrier.&lt;/li>
&lt;li>Once all threads have reached the barrier, they all proceed to print the second message.&lt;/li>
&lt;/ol>
&lt;p>&lt;strong>Important Notes&lt;/strong>&lt;/p>
&lt;ol>
&lt;li>
&lt;p>The &lt;code>barrier&lt;/code> directive is implicit at the end of &lt;code>parallel&lt;/code>, &lt;code>for&lt;/code>, &lt;code>sections&lt;/code>, and &lt;code>single&lt;/code> constructs, unless a &lt;code>nowait&lt;/code> clause is specified.&lt;/p>
&lt;/li>
&lt;li>
&lt;p>Using too many barriers can negatively impact performance, as threads may spend a lot of time waiting for each other.&lt;/p>
&lt;/li>
&lt;li>
&lt;p>Barriers should be used judiciously to ensure correct program behavior while minimizing synchronization overhead.&lt;/p>
&lt;/li>
&lt;li>
&lt;p>Be cautious of potential deadlocks when using barriers, especially in complex parallel regions.&lt;/p>
&lt;/li>
&lt;/ol>
&lt;p>The barrier directive is a powerful tool for synchronizing threads, but it should be used carefully to balance correctness and performance in your parallel programs.&lt;/p>
&lt;p>&lt;strong>Private Clause&lt;/strong>&lt;/p>
&lt;p>The &lt;code>private&lt;/code> clause is used to specify that each thread should have its own copy of the listed variables. This is useful when you want each thread to work with its own instance of a variable, preventing data races and ensuring thread safety.&lt;/p>
&lt;p>&lt;strong>Syntax&lt;/strong>&lt;/p>
&lt;blockquote>
&lt;p>#pragma omp parallel private(variable1, variable2, &amp;hellip;)&lt;/p>&lt;/blockquote>
&lt;p>or&lt;/p>
&lt;blockquote>
&lt;p>#pragma omp for private(variable1, variable2, &amp;hellip;)&lt;/p>&lt;/blockquote>
&lt;p>&lt;strong>Example&lt;/strong>&lt;/p>
&lt;p>Here&amp;rsquo;s an example demonstrating the use of the private clause:&lt;/p>
&lt;div class="collapsable-code">
&lt;input id="9" type="checkbox" checked />
&lt;label for="9">
&lt;span class="collapsable-code__language">c&lt;/span>
&lt;span class="collapsable-code__title">Private Clause&lt;/span>
&lt;span class="collapsable-code__toggle" data-label-expand="Show" data-label-collapse="Hide">&lt;/span>
&lt;/label>
&lt;pre class="language-c" >&lt;code>
#include &amp;lt;omp.h&amp;gt;
#include &amp;lt;stdio.h&amp;gt;
int main() {
int i, sum = 0;
#pragma omp parallel for private(i) reduction(&amp;#43;:sum)
for (i = 1; i &amp;lt;= 100; i&amp;#43;&amp;#43;) {
int temp = i * i;
sum &amp;#43;= temp;
printf(&amp;#34;Thread %d: i = %d, temp = %d\n&amp;#34;, omp_get_thread_num(), i, temp);
}
printf(&amp;#34;Sum of squares from 1 to 100: %d\n&amp;#34;, sum);
return 0;
}
&lt;/code>&lt;/pre>
&lt;/div>
&lt;p>&lt;strong>Output&lt;/strong>&lt;/p>
&lt;p>The output might look something like this (note that the order of thread execution may vary):&lt;/p>
&lt;pre tabindex="0">&lt;code>Thread 0: i = 1, temp = 1
Thread 0: i = 2, temp = 4
...
Thread 1: i = 26, temp = 676
Thread 1: i = 27, temp = 729
...
Thread 2: i = 51, temp = 2601
Thread 2: i = 52, temp = 2704
...
Thread 3: i = 76, temp = 5776
Thread 3: i = 77, temp = 5929
...
Sum of squares from 1 to 100: 338350
&lt;/code>&lt;/pre>&lt;p>&lt;strong>Explanation&lt;/strong>&lt;/p>
&lt;ol>
&lt;li>The &lt;code>i&lt;/code> variable is declared as private. This means each thread gets its own copy of &lt;code>i&lt;/code>.&lt;/li>
&lt;li>Inside the parallel for loop, each thread works with its own &lt;code>i&lt;/code>, preventing any data races.&lt;/li>
&lt;li>The &lt;code>temp&lt;/code> variable is implicitly private because it&amp;rsquo;s declared inside the parallel region.&lt;/li>
&lt;li>Each thread calculates the square of its assigned numbers and adds them to the shared &lt;code>sum&lt;/code> variable (using the reduction clause, which we&amp;rsquo;ll cover in a future topic).&lt;/li>
&lt;/ol>
&lt;p>&lt;strong>Important Notes&lt;/strong>&lt;/p>
&lt;ol>
&lt;li>
&lt;p>Variables declared as private are not initialized. They contain garbage values when entering the parallel region unless explicitly initialized.&lt;/p>
&lt;/li>
&lt;li>
&lt;p>Changes made to private variables are not visible outside the parallel region.&lt;/p>
&lt;/li>
&lt;li>
&lt;p>If you need to initialize private variables or use their values after the parallel region, consider using the &lt;code>firstprivate&lt;/code> or &lt;code>lastprivate&lt;/code> clauses (which we&amp;rsquo;ll cover next).&lt;/p>
&lt;/li>
&lt;li>
&lt;p>Array sections can also be made private using the syntax &lt;code>private(array[start:length])&lt;/code>.&lt;/p>
&lt;/li>
&lt;li>
&lt;p>The &lt;code>private&lt;/code> clause is often used with loop index variables to ensure each thread has its own copy of the loop counter.&lt;/p>
&lt;/li>
&lt;/ol>
&lt;p>The &lt;code>private&lt;/code> clause is a fundamental tool in OpenMP for managing data in parallel regions, helping to prevent data races and ensure correct parallel execution.&lt;/p>
&lt;p>&lt;strong>Firstprivate Clause&lt;/strong>&lt;/p>
&lt;p>The &lt;code>firstprivate&lt;/code> clause is similar to the &lt;code>private&lt;/code> clause, but it also initializes the private copies with the value of the original variable before entering the parallel region.&lt;/p>
&lt;p>&lt;strong>Syntax&lt;/strong>&lt;/p>
&lt;blockquote>
&lt;p>#pragma omp parallel firstprivate(variable1, variable2, &amp;hellip;)&lt;/p>&lt;/blockquote>
&lt;p>or&lt;/p>
&lt;blockquote>
&lt;p>#pragma omp for firstprivate(variable1, variable2, &amp;hellip;)&lt;/p>&lt;/blockquote>
&lt;p>&lt;strong>Example&lt;/strong>&lt;/p>
&lt;p>Here&amp;rsquo;s an example demonstrating the use of the firstprivate clause:&lt;/p>
&lt;div class="collapsable-code">
&lt;input id="9" type="checkbox" checked />
&lt;label for="9">
&lt;span class="collapsable-code__language">c&lt;/span>
&lt;span class="collapsable-code__title">Private Clause&lt;/span>
&lt;span class="collapsable-code__toggle" data-label-expand="Show" data-label-collapse="Hide">&lt;/span>
&lt;/label>
&lt;pre class="language-c" >&lt;code>
#include &amp;lt;omp.h&amp;gt;
#include &amp;lt;stdio.h&amp;gt;
int main() {
int x = 5;
#pragma omp parallel firstprivate(x)
{
printf(&amp;#34;Thread %d: x initially = %d\n&amp;#34;, omp_get_thread_num(), x);
x &amp;#43;= omp_get_thread_num();
printf(&amp;#34;Thread %d: x after modification = %d\n&amp;#34;, omp_get_thread_num(), x);
}
printf(&amp;#34;Outside parallel region: x = %d\n&amp;#34;, x);
return 0;
}
&lt;/code>&lt;/pre>
&lt;/div>
&lt;p>&lt;strong>Output&lt;/strong>&lt;/p>
&lt;p>The output might look something like this:&lt;/p>
&lt;pre tabindex="0">&lt;code>Thread 0: x initially = 5
Thread 1: x initially = 5
Thread 2: x initially = 5
Thread 3: x initially = 5
Thread 0: x after modification = 5
Thread 1: x after modification = 6
Thread 2: x after modification = 7
Thread 3: x after modification = 8
Outside parallel region: x = 5
&lt;/code>&lt;/pre>&lt;p>&lt;strong>Explanation&lt;/strong>&lt;/p>
&lt;ol>
&lt;li>The variable &lt;code>x&lt;/code> is initialized to 5 before the parallel region.&lt;/li>
&lt;li>Using &lt;code>firstprivate(x)&lt;/code>, each thread gets its own copy of &lt;code>x&lt;/code>, initialized to 5.&lt;/li>
&lt;li>Each thread then modifies its private copy of &lt;code>x&lt;/code> by adding its thread number.&lt;/li>
&lt;li>After the parallel region, the original &lt;code>x&lt;/code> remains unchanged (still 5).&lt;/li>
&lt;/ol>
&lt;p>&lt;strong>Important Notes&lt;/strong>&lt;/p>
&lt;ol>
&lt;li>
&lt;p>&lt;code>firstprivate&lt;/code> is particularly useful when you need to use the initial value of a variable in a parallel region, but also want each thread to have its own copy.&lt;/p>
&lt;/li>
&lt;li>
&lt;p>For complex objects (like C++ objects with constructors), &lt;code>firstprivate&lt;/code> ensures that the copy constructor is called to initialize the private copies.&lt;/p>
&lt;/li>
&lt;li>
&lt;p>&lt;code>firstprivate&lt;/code> can have a performance overhead compared to &lt;code>private&lt;/code>, especially for large objects, as it needs to initialize each private copy.&lt;/p>
&lt;/li>
&lt;li>
&lt;p>Changes made to firstprivate variables are not visible outside the parallel region unless you use other mechanisms (like reduction or lastprivate).&lt;/p>
&lt;/li>
&lt;li>
&lt;p>You can use &lt;code>firstprivate&lt;/code> with arrays, but be cautious with large arrays as it will create a full copy for each thread.&lt;/p>
&lt;/li>
&lt;/ol>
&lt;p>&lt;strong>Comparison with &lt;code>private&lt;/code>&lt;/strong>&lt;/p>
&lt;ul>
&lt;li>&lt;code>private&lt;/code> creates uninitialized copies for each thread.&lt;/li>
&lt;li>&lt;code>firstprivate&lt;/code> creates initialized copies, with the value from before entering the parallel region.&lt;/li>
&lt;/ul>
&lt;p>The &lt;code>firstprivate&lt;/code> clause is very useful when you need to work with initialized private copies in your parallel regions, combining the benefits of data isolation with proper initialization.&lt;/p>
&lt;h3 id="lastprivate-clause">&lt;strong>Lastprivate Clause&lt;/strong>&lt;/h3>
&lt;p>The &lt;code>lastprivate&lt;/code> clause is used to specify that the value of one or more private variables should be copied back to the original variables after the parallel region or loop. Specifically, it copies the value from the last logical iteration of the loop or the last section of a sections construct.&lt;/p>
&lt;p>&lt;strong>Syntax&lt;/strong>&lt;/p>
&lt;blockquote>
&lt;p>#pragma omp parallel for lastprivate(variable1, variable2, &amp;hellip;)&lt;/p>&lt;/blockquote>
&lt;p>or&lt;/p>
&lt;blockquote>
&lt;p>#pragma omp sections lastprivate(variable1, variable2, &amp;hellip;)&lt;/p>&lt;/blockquote>
&lt;p>&lt;strong>Example&lt;/strong>&lt;/p>
&lt;p>Here&amp;rsquo;s an example demonstrating the use of the lastprivate clause:&lt;/p>
&lt;div class="collapsable-code">
&lt;input id="9" type="checkbox" checked />
&lt;label for="9">
&lt;span class="collapsable-code__language">c&lt;/span>
&lt;span class="collapsable-code__title">Private Clause&lt;/span>
&lt;span class="collapsable-code__toggle" data-label-expand="Show" data-label-collapse="Hide">&lt;/span>
&lt;/label>
&lt;pre class="language-c" >&lt;code>
#include &amp;lt;omp.h&amp;gt;
#include &amp;lt;stdio.h&amp;gt;
int main() {
int i, x = 0;
#pragma omp parallel for lastprivate(x)
for (i = 0; i &amp;lt; 100; i&amp;#43;&amp;#43;) {
x = i;
printf(&amp;#34;Thread %d: i = %d, x = %d\n&amp;#34;, omp_get_thread_num(), i, x);
}
printf(&amp;#34;After parallel for: x = %d\n&amp;#34;, x);
return 0;
}
&lt;/code>&lt;/pre>
&lt;/div>
&lt;p>&lt;strong>Output&lt;/strong>&lt;/p>
&lt;p>The output might look something like this (note that the order of thread execution may vary):&lt;/p>
&lt;pre tabindex="0">&lt;code>Thread 0: i = 0, x = 0
Thread 1: i = 25, x = 25
Thread 2: i = 50, x = 50
Thread 3: i = 75, x = 75
...
Thread 3: i = 99, x = 99
After parallel for: x = 99
&lt;/code>&lt;/pre>&lt;p>&lt;strong>Explanation&lt;/strong>&lt;/p>
&lt;ol>
&lt;li>The variable &lt;code>x&lt;/code> is declared as lastprivate.&lt;/li>
&lt;li>Each thread has its own private copy of &lt;code>x&lt;/code> during the parallel for loop.&lt;/li>
&lt;li>After the loop completes, the value of &lt;code>x&lt;/code> from the last logical iteration (i = 99) is copied back to the original &lt;code>x&lt;/code>.&lt;/li>
&lt;li>Outside the parallel region, &lt;code>x&lt;/code> has the value from the last iteration (99).&lt;/li>
&lt;/ol>
&lt;p>&lt;strong>Important Notes&lt;/strong>&lt;/p>
&lt;ol>
&lt;li>
&lt;p>&lt;code>lastprivate&lt;/code> is particularly useful when you need the final value of a variable after a parallel loop or sections construct.&lt;/p>
&lt;/li>
&lt;li>
&lt;p>For loops, the &amp;ldquo;last&amp;rdquo; iteration is determined by the logical order of the loop, not the actual last thread to finish.&lt;/p>
&lt;/li>
&lt;li>
&lt;p>For &lt;code>sections&lt;/code> constructs, the last section in lexical order determines the lastprivate value.&lt;/p>
&lt;/li>
&lt;li>
&lt;p>&lt;code>lastprivate&lt;/code> can be combined with &lt;code>firstprivate&lt;/code> if you need both initialization and final value preservation.&lt;/p>
&lt;/li>
&lt;li>
&lt;p>There&amp;rsquo;s a potential performance overhead with &lt;code>lastprivate&lt;/code>, as it requires additional synchronization to determine and copy the final value.&lt;/p>
&lt;/li>
&lt;/ol>
&lt;p>&lt;strong>Comparison with &lt;code>private&lt;/code> and &lt;code>firstprivate&lt;/code>&lt;/strong>&lt;/p>
&lt;ul>
&lt;li>&lt;code>private&lt;/code>: Creates uninitialized copies, final values are not preserved.&lt;/li>
&lt;li>&lt;code>firstprivate&lt;/code>: Creates initialized copies, final values are not preserved.&lt;/li>
&lt;li>&lt;code>lastprivate&lt;/code>: May create uninitialized copies (unless combined with &lt;code>firstprivate&lt;/code>), but preserves the final value from the logically last iteration or section.&lt;/li>
&lt;/ul>
&lt;p>The &lt;code>lastprivate&lt;/code> clause is valuable when you need to capture the final state of a variable after parallel execution, especially in loops where the final iteration produces an important result.&lt;/p>
&lt;p>&lt;strong>Reduction Clause&lt;/strong>&lt;/p>
&lt;p>The &lt;code>reduction&lt;/code> clause is used to perform a reduction operation on one or more variables across all threads in a parallel region. It&amp;rsquo;s particularly useful for operations like summing, finding the maximum or minimum, or performing logical operations across a set of values computed in parallel.&lt;/p>
&lt;p>&lt;strong>Syntax&lt;/strong>&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-c" data-lang="c">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#75715e">#pragma omp parallel for reduction(operator:variable)
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Where &lt;code>operator&lt;/code> can be:&lt;/p>
&lt;ul>
&lt;li>&lt;code>+&lt;/code>, &lt;code>-&lt;/code>, &lt;code>*&lt;/code>, &lt;code>&amp;amp;&lt;/code>, &lt;code>|&lt;/code>, &lt;code>^&lt;/code>, &lt;code>&amp;amp;&amp;amp;&lt;/code>, &lt;code>||&lt;/code> for built-in types&lt;/li>
&lt;li>&lt;code>max&lt;/code>, &lt;code>min&lt;/code> for arithmetic types&lt;/li>
&lt;/ul>
&lt;p>&lt;strong>Example&lt;/strong>&lt;/p>
&lt;p>Here&amp;rsquo;s an example demonstrating the use of the reduction clause to calculate the sum of squares:&lt;/p>
&lt;div class="collapsable-code">
&lt;input id="9" type="checkbox" checked />
&lt;label for="9">
&lt;span class="collapsable-code__language">c&lt;/span>
&lt;span class="collapsable-code__title">Private Clause&lt;/span>
&lt;span class="collapsable-code__toggle" data-label-expand="Show" data-label-collapse="Hide">&lt;/span>
&lt;/label>
&lt;pre class="language-c" >&lt;code>
#include &amp;lt;omp.h&amp;gt;
#include &amp;lt;stdio.h&amp;gt;
int main() {
long long sum = 0;
int i, n = 1000000;
#pragma omp parallel for reduction(&amp;#43;:sum)
for (i = 1; i &amp;lt;= n; i&amp;#43;&amp;#43;) {
sum &amp;#43;= i * i;
}
printf(&amp;#34;Sum of squares from 1 to %d: %lld\n&amp;#34;, n, sum);
return 0;
}
&lt;/code>&lt;/pre>
&lt;/div>
&lt;p>&lt;strong>Output&lt;/strong>&lt;/p>
&lt;p>The output will be:&lt;/p>
&lt;pre tabindex="0">&lt;code>Sum of squares from 1 to 1000000: 333333833333500000
&lt;/code>&lt;/pre>&lt;p>&lt;strong>Explanation&lt;/strong>&lt;/p>
&lt;ol>
&lt;li>The &lt;code>sum&lt;/code> variable is specified as a reduction variable with the &lt;code>+&lt;/code> operator.&lt;/li>
&lt;li>Each thread computes a partial sum for its chunk of the loop.&lt;/li>
&lt;li>OpenMP automatically combines these partial sums using the specified operator (&lt;code>+&lt;/code> in this case).&lt;/li>
&lt;li>The final result is stored in the original &lt;code>sum&lt;/code> variable.&lt;/li>
&lt;/ol>
&lt;p>&lt;strong>Important Notes&lt;/strong>&lt;/p>
&lt;ol>
&lt;li>
&lt;p>OpenMP creates a private copy of the reduction variable for each thread, initialized according to the reduction operator (e.g., 0 for &lt;code>+&lt;/code>, 1 for &lt;code>*&lt;/code>, etc.).&lt;/p>
&lt;/li>
&lt;li>
&lt;p>At the end of the parallel region, these private copies are combined using the specified operator to produce the final result.&lt;/p>
&lt;/li>
&lt;li>
&lt;p>Reduction operations are both thread-safe and efficient, as they avoid the need for explicit synchronization.&lt;/p>
&lt;/li>
&lt;li>
&lt;p>You can specify multiple reduction variables in one clause:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-c" data-lang="c">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#75715e">#pragma omp parallel for reduction(+:sum1,sum2) reduction(max:maxval)
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;/li>
&lt;li>
&lt;p>Custom reduction operations can be defined for user-defined types in C++ using the &lt;code>declare reduction&lt;/code> directive.&lt;/p>
&lt;/li>
&lt;/ol>
&lt;p>&lt;strong>Common Use Cases&lt;/strong>&lt;/p>
&lt;ol>
&lt;li>Summing values: &lt;code>reduction(+:sum)&lt;/code>&lt;/li>
&lt;li>Finding maximum: &lt;code>reduction(max:max_value)&lt;/code>&lt;/li>
&lt;li>Finding minimum: &lt;code>reduction(min:min_value)&lt;/code>&lt;/li>
&lt;li>Logical AND of conditions: &lt;code>reduction(&amp;amp;&amp;amp;:all_true)&lt;/code>&lt;/li>
&lt;li>Logical OR of conditions: &lt;code>reduction(||:any_true)&lt;/code>&lt;/li>
&lt;/ol>
&lt;p>&lt;strong>Performance Considerations&lt;/strong>&lt;/p>
&lt;p>Reductions are generally very efficient, as they allow each thread to work independently and combine results only at the end. This often leads to good scalability. However, for very small loops or when the reduction operation is very simple, the overhead of creating and managing threads might outweigh the benefits of parallelization.&lt;/p>
&lt;p>The &lt;code>reduction&lt;/code> clause is a powerful feature in OpenMP that simplifies parallel aggregation operations and helps avoid common pitfalls like race conditions when accumulating results across threads.&lt;/p>
&lt;p>&lt;strong>Schedule Clause&lt;/strong>&lt;/p>
&lt;p>The &lt;code>schedule&lt;/code> clause is used to specify how iterations of a loop are divided among threads in a parallel region. It allows you to control the workload distribution, which can significantly impact the performance of your parallel program.&lt;/p>
&lt;p>&lt;strong>Syntax&lt;/strong>&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-c" data-lang="c">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#75715e">#pragma omp parallel for schedule(kind[, chunk_size])
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Where &lt;code>kind&lt;/code> can be:&lt;/p>
&lt;ul>
&lt;li>&lt;code>static&lt;/code>&lt;/li>
&lt;li>&lt;code>dynamic&lt;/code>&lt;/li>
&lt;li>&lt;code>guided&lt;/code>&lt;/li>
&lt;li>&lt;code>auto&lt;/code>&lt;/li>
&lt;li>&lt;code>runtime&lt;/code>&lt;/li>
&lt;/ul>
&lt;p>&lt;strong>Types of Schedules&lt;/strong>&lt;/p>
&lt;ol>
&lt;li>
&lt;p>&lt;strong>Static Schedule&lt;/strong>&lt;/p>
&lt;ul>
&lt;li>Iterations are divided into chunks of size &lt;code>chunk_size&lt;/code> and distributed to threads in a round-robin fashion.&lt;/li>
&lt;li>If &lt;code>chunk_size&lt;/code> is not specified, iterations are divided equally among threads.&lt;/li>
&lt;/ul>
&lt;/li>
&lt;li>
&lt;p>&lt;strong>Dynamic Schedule&lt;/strong>&lt;/p>
&lt;ul>
&lt;li>Iterations are divided into chunks of size &lt;code>chunk_size&lt;/code>.&lt;/li>
&lt;li>Each thread requests a new chunk when it finishes its current chunk.&lt;/li>
&lt;/ul>
&lt;/li>
&lt;li>
&lt;p>&lt;strong>Guided Schedule&lt;/strong>&lt;/p>
&lt;ul>
&lt;li>Similar to dynamic, but the chunk size starts large and decreases over time.&lt;/li>
&lt;li>The chunk size is proportional to the number of unassigned iterations divided by the number of threads.&lt;/li>
&lt;/ul>
&lt;/li>
&lt;li>
&lt;p>&lt;strong>Auto Schedule&lt;/strong>&lt;/p>
&lt;ul>
&lt;li>The decision of scheduling is delegated to the compiler and/or runtime system.&lt;/li>
&lt;/ul>
&lt;/li>
&lt;li>
&lt;p>&lt;strong>Runtime Schedule&lt;/strong>&lt;/p>
&lt;ul>
&lt;li>The schedule is determined at runtime by the &lt;code>OMP_SCHEDULE&lt;/code> environment variable.&lt;/li>
&lt;/ul>
&lt;/li>
&lt;/ol>
&lt;p>&lt;strong>Example&lt;/strong>&lt;/p>
&lt;p>Here&amp;rsquo;s an example demonstrating different schedule types:&lt;/p>
&lt;div class="collapsable-code">
&lt;input id="9" type="checkbox" checked />
&lt;label for="9">
&lt;span class="collapsable-code__language">c&lt;/span>
&lt;span class="collapsable-code__title">Private Clause&lt;/span>
&lt;span class="collapsable-code__toggle" data-label-expand="Show" data-label-collapse="Hide">&lt;/span>
&lt;/label>
&lt;pre class="language-c" >&lt;code>
#include &amp;lt;omp.h&amp;gt;
#include &amp;lt;stdio.h&amp;gt;
void test_schedule(const char* schedule_type) {
int i, n = 20;
#pragma omp parallel for schedule(runtime)
for (i = 0; i &amp;lt; n; i&amp;#43;&amp;#43;) {
printf(&amp;#34;Thread %d executing iteration %d\n&amp;#34;, omp_get_thread_num(), i);
}
printf(&amp;#34;\nTested with schedule type: %s\n\n&amp;#34;, schedule_type);
}
int main() {
const char* schedules[] = {&amp;#34;static&amp;#34;, &amp;#34;static,2&amp;#34;, &amp;#34;dynamic&amp;#34;, &amp;#34;dynamic,2&amp;#34;, &amp;#34;guided&amp;#34;, &amp;#34;auto&amp;#34;};
int num_schedules = sizeof(schedules) / sizeof(schedules[0]);
for (int i = 0; i &amp;lt; num_schedules; i&amp;#43;&amp;#43;) {
omp_set_schedule(omp_sched_runtime, 0);
setenv(&amp;#34;OMP_SCHEDULE&amp;#34;, schedules[i], 1);
test_schedule(schedules[i]);
}
return 0;
}
&lt;/code>&lt;/pre>
&lt;/div>
&lt;p>&lt;strong>Choosing the Right Schedule&lt;/strong>&lt;/p>
&lt;ul>
&lt;li>Use &lt;code>static&lt;/code> when iterations have similar workloads and you want to minimize scheduling overhead.&lt;/li>
&lt;li>Use &lt;code>dynamic&lt;/code> or &lt;code>guided&lt;/code> when iterations have varying workloads to achieve better load balancing.&lt;/li>
&lt;li>Use &lt;code>auto&lt;/code> when you&amp;rsquo;re unsure and want the system to decide.&lt;/li>
&lt;li>Use &lt;code>runtime&lt;/code> for flexibility in testing different schedules without recompiling.&lt;/li>
&lt;/ul>
&lt;p>The choice of schedule can significantly affect performance, especially for loops with irregular workloads or when dealing with NUMA (Non-Uniform Memory Access) architectures.&lt;/p></content></item><item><title>Understanding floating point numbers</title><link>https://blog.narain.dev/posts/float32/</link><pubDate>Fri, 21 Jun 2024 16:24:31 -0700</pubDate><guid>https://blog.narain.dev/posts/float32/</guid><description>&lt;h2 id="introduction">introduction&lt;/h2>
&lt;p>In 1991, a Patriot missile defense system in Saudi Arabia failed to intercept an incoming Scud missile,
resulting in the tragic deaths of 28 American soldiers. The cause? A tiny floating point error that
accumulated over time, throwing off the system&amp;rsquo;s timing by just 0.34 seconds. This incident highlights the
critical importance of understanding floating point numbers in computer systems. Floating point representation
allows computers to work with a wide range of real numbers, from the microscopic to the astronomical.
However, it also introduces complexities and potential inaccuracies that programmers must carefully navigate.&lt;/p></description><content>&lt;h2 id="introduction">introduction&lt;/h2>
&lt;p>In 1991, a Patriot missile defense system in Saudi Arabia failed to intercept an incoming Scud missile,
resulting in the tragic deaths of 28 American soldiers. The cause? A tiny floating point error that
accumulated over time, throwing off the system&amp;rsquo;s timing by just 0.34 seconds. This incident highlights the
critical importance of understanding floating point numbers in computer systems. Floating point representation
allows computers to work with a wide range of real numbers, from the microscopic to the astronomical.
However, it also introduces complexities and potential inaccuracies that programmers must carefully navigate.&lt;/p>
&lt;p>It is important to understand the way we represent floating point numbers in computers to avoid unwanted edge
cases in programming. if you are considering to use float32 for managing your finances just because you would be able
to represent cents be mindful that IEEE 754 skips a number after 2^24 (16,777,217). At this point you are not only missing
numbers but errors due to calculations keeps on adding up.&lt;/p>
&lt;pre tabindex="0">&lt;code>Disclaimer Throughout this writeup i will be consistently switching between hardware and its usecases in
writing applications as we are bound by the hardware design
&lt;/code>&lt;/pre>&lt;h3 id="how-is-floating-point-numbers-represented-in-binary">how is floating point numbers represented in binary&lt;/h3>
&lt;figure class="center" >
&lt;img src="https://blog.narain.dev/images/floatvsint.png" alt="Binary Representations" />
&lt;figcaption class="center" >Binary Representations&lt;/figcaption>
&lt;/figure>
&lt;p>sing base-2 (binary) representation for floating point numbers minimizes rounding errors compared to
higher bases. Higher base systems often introduce unused bits, leading to inefficient memory usage.
Furthermore, binary representation aligns with how electronic systems naturally process and store
data, making it the optimal choice for computational tasks&lt;/p>
&lt;p>Real numbers are infinite between any two integers, but representing them on computers with
fixed bit widths involves trade-offs. The density of floating point numbers decays
exponentially as we move away from zero, meaning that more precision is available near
zero and less as values grow larger. Squeezing infinitely many real numbers into a
fixed bit width necessitates approximations. As a result, there are more representable numbers
between -2 and 2 than in other regions of the number line.&lt;/p>
&lt;div class="collapsable-code">
&lt;input id="1" type="checkbox" checked />
&lt;label for="1">
&lt;span class="collapsable-code__language">c&lt;/span>
&lt;span class="collapsable-code__title">floating point numbers skipping&lt;/span>
&lt;span class="collapsable-code__toggle" data-label-expand="show" data-label-collapse="hide">&lt;/span>
&lt;/label>
&lt;pre class="language-c" >&lt;code>
#include &amp;lt;stdio.h&amp;gt;
#include &amp;lt;math.h&amp;gt;
int main() {
float a = (float)pow(2, 24);
for (int i = 1; i &amp;lt; 10; i&amp;#43;&amp;#43;) {
printf(&amp;#34;%d, %f\n&amp;#34;, i, a &amp;#43; i);
}
}
// 2**24 &amp;#43; 1 = 16777216.000000
// 2**24 &amp;#43; 2 = 16777218.000000
// 2**24 &amp;#43; 3 = 16777220.000000
// 2**24 &amp;#43; 4 = 16777220.000000
// 2**24 &amp;#43; 5 = 16777220.000000
// 2**24 &amp;#43; 6 = 16777222.000000
// 2**24 &amp;#43; 7 = 16777224.000000
// 2**24 &amp;#43; 8 = 16777224.000000
// 2**24 &amp;#43; 9 = 16777224.000000
&lt;/code>&lt;/pre>
&lt;/div>
&lt;p>From the above example we observe that as numbers get larger the spacing between consecutive
representable numbers increases. The above case at 2^24 floating point format cannot represent every
integer precisely beyond this point.&lt;/p>
&lt;ul>
&lt;li>2^24 + 1 the floating point precision is insufficient to capture the difference of just 1 due to rounding error&lt;/li>
&lt;li>2^24 + 2 the floating point precision allows 16777218 to be represented and skips over 16777217.&lt;/li>
&lt;/ul>
&lt;p>this phenomenon is called as &lt;code>floating point precision loss&lt;/code> at higher magnitudes, more integers are
skipped because the system can&amp;rsquo;t represent every possible number within the limited floating point precision.&lt;/p>
&lt;pre tabindex="0">&lt;code>FACT:
javascript uses float64 to represent all numeric data types
&lt;/code>&lt;/pre>&lt;h2 id="history-of-floating-point-numbers">History of Floating point numbers&lt;/h2>
&lt;p>Initially, computers only supported integer arithmetic, as it was sufficient for early applications.
However, as computational needs grew, so did the necessity to represent and manipulate
decimal numbers. To address this, programmers began emulating floating-point arithmetic through
complex algorithms, which required more operations than simple integer addition or subtraction.&lt;/p>
&lt;p>&lt;a href="https://gist.github.com/narain1/1520a67646d365bd6260914ff5233bec">Float Emulator&lt;/a>&lt;/p>
&lt;p>Recognizing the inefficiency of these emulations, chip manufacturers started designing dedicated hardware, such
as floating-point registers, to execute floating-point instructions in a single cycle. These
registers adhere to the IEEE 754 standard, which defines the floating-point format with 1 sign
bit, 23 mantissa bits, and 8 exponent bits. Sticking to this standard is critical for
ensuring consistency across platforms, allowing programmers to achieve identical results on different
systems—vital for reliable and predictable computation.&lt;/p>
&lt;h5 id="evolution">Evolution&lt;/h5>
&lt;p>With the rising concern of the &lt;code>memory wall&lt;/code> chip manufacturers build custom low precision floating point
arithmatic such as float8, float16, bfloat16 and tensor-float32.&lt;/p>
&lt;p>where float32 is the standard representation&lt;/p>
&lt;h2 id="efficiency-to-high-precision-calculations">Efficiency to high precision calculations&lt;/h2>
&lt;p>Floating-point operations, due to the limitations of their representation, come with some inherent drawbacks.
One key issue is that they are not strictly commutative—meaning a+b≠b+a in certain cases, particularly in
low-precision arithmetic. This non-commutativity increases rounding errors, especially in
embarrassingly parallel applications, where threads process operands out of order.
While floating-point arithmetic typically works well in serial execution, issues arise when code is
parallelized or when the order of operations (like in loops) is changed, leading to slight variations in results.&lt;/p>
&lt;p>Because of these precision issues, it is a common practice not to validate floating-point results by checking
for exact equality. Instead, computations are often considered correct if the results fall within a specified
tolerance range, ensuring that small rounding errors do not invalidate the outcome.&lt;/p>
&lt;h2 id="further-reads">Further reads&lt;/h2>
&lt;ul>
&lt;li>&lt;a href="https://nhigham.com/2020/05/04/what-is-floating-point-arithmetic/">Whis is floating point arithmetic&lt;/a>&lt;/li>
&lt;li>&lt;a href="https://randomascii.wordpress.com/2013/07/16/floating-point-determinism/">Floating point determinism&lt;/a>&lt;/li>
&lt;li>&lt;a href="https://yosefk.com/blog/consistency-how-to-defeat-the-purpose-of-ieee-floating-point.html">How to defeat the purpose of IEEE floating point&lt;/a>&lt;/li>
&lt;/ul></content></item><item><title>Distributed Training</title><link>https://blog.narain.dev/posts/torchdistributed/</link><pubDate>Sun, 07 Apr 2024 12:44:46 -0700</pubDate><guid>https://blog.narain.dev/posts/torchdistributed/</guid><description>&lt;p>Training advanced neural networks, particularly deep learning models such as transformers, presents significant computational challenges.
These sophisticated models require substantial computational power, often exceeding the capabilities of the single GPU machine. The primary
hurdles stem from the immense number of parameters that need to be calculated and updated during the training process. This not only demands
extensive GPU memory but also nencassitates the use of large batch sizes to achieve efficient learning and convergence.&lt;/p></description><content>&lt;p>Training advanced neural networks, particularly deep learning models such as transformers, presents significant computational challenges.
These sophisticated models require substantial computational power, often exceeding the capabilities of the single GPU machine. The primary
hurdles stem from the immense number of parameters that need to be calculated and updated during the training process. This not only demands
extensive GPU memory but also nencassitates the use of large batch sizes to achieve efficient learning and convergence.&lt;/p>
&lt;p>As the complexity of models grows, So does the need for more advanced hardware accelerators. GPUs (Graphics Processing Units) have
become essential in the field of deep learning due to their ability to handle parallel computations effectively. These accelerators
significantly reduce training time by distributing the computational load across multiple cores. GPUs whith their high throughput, are particularly
well-suited for tasks involving large batch sizes and extensive data parallelism. These accelerators are invaluable for training extremely large models
that would otherwise be infeasible on traditional hardware.&lt;/p>
&lt;p>The necassity for such powerful accelerators highlights the ever growing demands of deep learning technologies and sets the stage for exploring
distributed training methods, which aim to further amplify the capabilities of induvidual training sessions by leveraging multiple GPUs or TPUs
simultaneosly.&lt;/p>
&lt;h2 id="essentials-of-distributed-training">Essentials of distributed Training&lt;/h2>
&lt;p>Lets have a refresher on how deep learning models work. Deep Learning models operate on a mechanism known as gradient descent, a fundamental
technique used to optimize these models by mimicking a data representations. During the training process, a model adjusts its internal
parameters or weights based on the difference between its predictions and the actual outputs. This difference is qualified through a loss
function, which measures the model performance. The backward pass of training involves updating these weights by calculating gradients, which
are essentially directions and magnitudes for improving model predictions.&lt;/p>
&lt;figure class="center" >
&lt;img src="https://blog.narain.dev/images/deep-learning1.png" alt="fully sharded data parallel" style="border-radius: 8px;" />
&lt;figcaption class="center" >conventional deep learning flow&lt;/figcaption>
&lt;/figure>
&lt;p>To manage these gradients, optimizers like Adam are employed. These optimizers not only update the weights based on the gradients but also
keep track of past gradient values to make informed adjustments, enhancing the efficiency and stability of the learning process.&lt;/p>
&lt;p>In the context of distributed deep learning, the training process is expanded across multiple machines. This setup requires a synchronized
forward pass, where each device uses the same model weights but different subsets of input data. This approach enhances the model&amp;rsquo;s ability
to generalize across diverse data sets and helps mitigate the influence of outliers.&lt;/p>
&lt;p>After the forward pass, gradients calculated on different machines are collected and aggregated. This aggregation is crucial it ensures that
all partial insights from the distributed datasets are combined to update the model in a comprehensive manner. Once the weights are updated,
they are redistributed to all devices to maintain consistency across the model for subsequent training iterations.&lt;/p>
&lt;p>This distributed approach not only speeds up the training process by parallelizing tasks but also allows for handlinf larger models and datasets
that would be impractical to process on a single machine.&lt;/p>
&lt;h3 id="scaling-up-with-distributed-techniques">Scaling Up with distributed techniques&lt;/h3>
&lt;p>The need to train increasingle large models, some with billions of parameters, has necassitated a shift towards distributed training across
multiple GPUs. This shift is driven by the fact that many advanced models cannot fit into memory of a single GPU. Instead, distributing
the computational workload across sevaral GPUs offers a feasible solution. The practive of distributed training gained traction when
CNNs were being trained on large datasets like ImageNet. In such networks, the initial convolutional layers are particularly computation-intensive
due to the stencil operation required to process the entire image. This realization led to early efforts in distributed training focusing primarily
on dividing the compute heavy tasks among multiple GPUs. This kind of splitting model parameters across multiple GPUs is called model parallelism.&lt;/p>
&lt;p>parallelism in deep learning perspective is of the following types:&lt;/p>
&lt;ul>
&lt;li>model parallel&lt;/li>
&lt;li>data parallel&lt;/li>
&lt;li>tensor parallel&lt;/li>
&lt;/ul>
&lt;figure class="center" >
&lt;img src="https://blog.narain.dev/images/deep-learning2.png" alt="model parallel" style="border-radius: 8px;" />
&lt;figcaption class="center" >Model Parallel&lt;/figcaption>
&lt;/figure>
&lt;p>model parallel is the easiest to implement as we dont have multiple replicas of the same parameter and no reduction is required. The only change
required from the normal training routine is at some point the intermediate outputs need to be transferred from one GPU to the other.&lt;/p>
&lt;h3 id="data-parallelism">Data Parallelism&lt;/h3>
&lt;p>data parallelism is a cornerstone of distributed training strategies, especially when dealing with extensive deep learning models. In this approach,
each GPU holds a complete copy of the model, ensuring that every unit can independently process different subsets of the overall dataset.
While this method effectively utilizes the parallel nature of GPUs, it introduces significant complexity during the backward pass, particularly
in gradient accumulation and synchronization.&lt;/p>
&lt;p>The challenge arises from the need to integrate gradients computed induvidually on each GPU. Since different GPUs process different data,
the generate unique gradients, which must be averaged before updating the model&amp;rsquo;s weights. This averaging is crucial because it
ensures that the model learns uniformly from the entire dataset rather than just fragments of it.&lt;/p>
&lt;blockquote>
&lt;p>There was a trick used to train models of larger batch size than possible using gradient accumlation, this method computes gradients every batch
but only updates the weights interleaved.&lt;/p>&lt;/blockquote>
&lt;p>Several methods are employed to manage this averaging process across machines. A commonly used technique involves a parameter server architecture.
In this setup, a central server aggregates the gradients from all GPU&amp;rsquo;s. This method, however has scalability limitations. As the number of GPUs
increases often going beyond ten the parameter server can become a bottleneck, straining under the heavy load of receiving and processing gradients
from each unit. This can potentially halt the training process, affecting efficiency and scalability.&lt;/p>
&lt;figure class="center" >
&lt;img src="https://blog.narain.dev/images/paramter-server.png" alt="parameter server" style="border-radius: 8px;" />
&lt;figcaption class="center" >Parameter server&lt;/figcaption>
&lt;/figure>
&lt;p>Frameworks like tensorflow have addresed some of these issues by implementing advanced versions of the parameter server model, which are designed
to handle large clussters more effectively. In addition to the challenges of gradient averaging, data parallelism must also manage synchronization
issues that arise when nodes vary in computational speed. This variation can introduce a problem known as staleness, where the updates from
slower nodes lag behind those from faster ones.&lt;/p>
&lt;p>During a distributed training session, each GPU (or node) processes a different batch of data and computes gradients independently. Ideally, these
gradients are synchronized across all nodes to update the model weights uniformly. However, if one node takes significantly longer to process its
batch, it causes a delay in the aggregation of gradients. As a result, faster nodes must wait for the slower ones to finish computation, leading to
idle time that can significantly reduce overall system efficiency.&lt;/p>
&lt;blockquote>
&lt;p>There is additional overhead when dealing with fault tolerency in these synchronization, but that is beyond the scope of this discussion.&lt;/p>&lt;/blockquote>
&lt;p>This staleness not only affect the synchronization step but can also impact the accuracy and convergence of the model. If gradients from slower
nodes are outdated by the time they are integrated, they might not accurately represent the current state of the model leading to suboptimal updates.&lt;/p>
&lt;figure class="center" >
&lt;img src="https://blog.narain.dev/images/model-synchronization1.png" alt="Model synchronization" style="border-radius: 8px;" />
&lt;figcaption class="center" >Model Synchronization&lt;/figcaption>
&lt;/figure>
&lt;p>To mitigate these issues, various synchronization strategies are employed. One common approach is to use barrier synchronization, where all nodes
must reach a certain point in the computation before any of them can proceed. This ensures that all gradient updates are based on the same version
of the model, preventing staleness. However this method can lead to increased waiting times, as all nodes are forced to operate at the pace
of the slowest one.&lt;/p>
&lt;p>Another approach is to implement asynchronous updates, where nodes update the central model with their gradients as soon as they are ready, without
waiting for the slowest nodes. This mothod can improve the efficiency of the system by reducing idle time but at the cost of introducing more
staleness into model updates, which can complicate convergence.&lt;/p>
&lt;p>Despite These bottleneck issue can still pose significant challenges, prompting the development and adoption of more sophisticated techniques
such as ring-allreduce. This method distributes the task of gradient aggregation across all devices, therby eliminating the need for a central
parameter server and significantly reducing the communication overhead.&lt;/p>
&lt;h2 id="horovod">Horovod&lt;/h2>
&lt;p>Horovod is a distributed training framework introduced by Uber by modifying the common ring-reduce algorithm this significantly enhances
the efficiency of data synchronization across multiple GPUs by employing the ring allreduce algorithm. This method is specifically designed
to minimize the communication overhead by reducing the number of simultaneous data transfers, thus avoiding bandwidth saturation and
ensuring a stable and fast data exchange.&lt;/p>
&lt;figure class="center" >
&lt;img src="https://blog.narain.dev/images/ring-allreduce.png" alt="fully sharded data parallel" style="border-radius: 8px;" />
&lt;figcaption class="center" >ring all-reduce&lt;/figcaption>
&lt;/figure>
&lt;p>The ring-allreduce algorithm operates by organizing each GPU into a virtual ring topology. It begins by dividing the gradient data into
equal segments and assigning each segment to a GPU. In this setup, each node is responsible for sending a segment of its data to its
neighbor on the right and simultaneously recieving a segment from its neighbor on the left. Upon recieving a segment, a node quickly combines it
with its correspponding segment a process known as reduction. After reducing the data, the node passes the resultant data to the next node in the
ring. This process of rotating and reducing continues until every segment has circulated back to its original node.&lt;/p>
&lt;p>By the end of this cyclem each GPU has recieved segments of data from all other GPUs, ensuring that each segment is fully reduced set of gradients
from across the network. These averaged gradients are then used to update the model weights on each GPU, synchronizing the model state across all
nodes to maintain uniformly for the next training iteration.&lt;/p>
&lt;p>One of the primary benifits of horovod is its efficient use of network resources. The framework manages to distribute the communication
load evenly across all participating GPUs. By transferring the weights in a round-robin fashion and processing the data in manageble parts,
Horovod stays within the available bandwidth limits. This careful management helps in reducing network congestion and ensures a smoother
gradient averaging process making horovod particularly advantageous in environments where network resources are constraint.&lt;/p>
&lt;h1 id="exploring-pytorchs-approach-to-distributed-training">Exploring PyTorch&amp;rsquo;s Approach to Distributed training&lt;/h1>
&lt;p>Pytorch offers a robust distributed training framework that differs notably from Horovod&amp;rsquo;s methodology. Unline Horovod, which synchronizes
gradients after they are computed across all GPUs Pytorch synchronizes gradients as soon as they become available, facilitating a more
dynamic and efficient workflow. This immediate synchronization is part of PyTorch&amp;rsquo;s Distributed Data Parallel (DDP) architecture, which is designed
to enhance performance, particularly in environments where bandwidth might be a limiting factor [it always is].&lt;/p>
&lt;p>The key to Pytorch&amp;rsquo;s efficiency lies in its ability to overlap gradient computation with gradient synchronization. Here&amp;rsquo;s how this process
typically unfolds: pytorch groups gradients into what are called buckets. as soon as one of these buckets are filled with computed gradients, it
doesn&amp;rsquo;t wait for the rest of the model to finish computing its gradients. Instead it immediately begins the synchronization process for that bucket.
This method allows DDP to initialize the communication phase much earlier than if it had to wait for the entire backward pass to complete.&lt;/p>
&lt;p>This overlapping of computation and communication is highly benificial. It ensures that the GPUs are not left idle waiting for data, thus making
full use of the network bandwidth and reducing overall training time. By starting the synchronization process as a portion of the gradients is
ready, pytorch minimizes the downtime that each GPU might otherwise experience if it had to wait for the entire network&amp;rsquo;s gradients to be computed
befire beginning synchronization.&lt;/p>
&lt;p>Pytorch provides two common methods are used to facilitate the training of deep learning models across multiple GPUs: Data Parallel (DP) and
Distributed Data Parallel (DDP). While both approaches aim to distribute the training workload across several computing units, They are
fundamentally different in how they manage and execute this distribution.&lt;/p>
&lt;h2 id="distributed-data-parallel-ddp">Distributed Data Parallel (DDP):&lt;/h2>
&lt;p>Distrbuted Data Parallel, is optimized for both single machine and multi machine configurations. DDP improves upon the basic idea od DP by
reducing the communication bottleneck observed in DP. It does this by eliminating the need for a master GPU. Instead, each GPU independently
computes the gradients from its subset of the data, and then a collective communication operation (like all-reduce) is employed to average these
gradients across all devices without funneling through a single point. DDP uses processes instead of theads for handling parallel tasks across
multiple GPUs. This design choice is pivotal for enhancing performance and avoiding common pitfalls associated with multithreading in python,
such as Global Interpreter Lock (GIL) constraints. Additionally, processes improve scalability across varied hardware and network
architectures. They support robust and reliable communication mechanisms, such as NCCL, MPI. These are specifically optimized for inter
process communication, enhancing the overall efficiency and effictiveness of distributed training operations.&lt;/p>
&lt;h3 id="some-key-words-of-ddp-are">Some Key words of DDP are:&lt;/h3>
&lt;ul>
&lt;li>WORLD_SIZE: defines the total number of workers n1 nodes n2 gpus = n1 * n2&lt;/li>
&lt;li>WORLD_RANK: identifies the worker ranges from 0..(n1*n2)&lt;/li>
&lt;li>LOCAL_RANK: defines the id of a worker within a node or indirectly refers to the gpu rank&lt;/li>
&lt;/ul>
&lt;h1 id="references">References&lt;/h1>
&lt;ul>
&lt;li>&lt;a href="https://static.googleusercontent.com/media/research.google.com/en//archive/large_deep_networks_nips2012.pdf">Distbilief Paper&lt;/a>&lt;/li>
&lt;li>&lt;a href="https://arxiv.org/pdf/2006.15704">Pytorch Distributed: Experiences on Accelerating Data Parallel Training&lt;/a>&lt;/li>
&lt;li>&lt;a href="https://www.microsoft.com/en-us/research/blog/zero-deepspeed-new-system-optimizations-enable-training-models-with-over-100-billion-parameters/">Deep Speed Zero&lt;/a>&lt;/li>
&lt;li>&lt;/li>
&lt;/ul></content></item><item><title>About</title><link>https://blog.narain.dev/about/</link><pubDate>Fri, 05 Apr 2024 21:11:49 -0700</pubDate><guid>https://blog.narain.dev/about/</guid><description>&lt;img src="https://blog.narain.dev/images/narain1.jpg" alt="Hello Friend" class="center" style="border-radius: 8px; width: 300px; height: auto;" />
&lt;p>Hello! I’m Narain. I tinker with machine learning models for a living, diving into the nuts and bolts of deep learning,
GPU acceleration, and cloud infrastructures. When I&amp;rsquo;m not busy breaking stuff, I might be pushing the boundaries
of what should (or shouldn’t) be done in C++.&lt;/p>
&lt;p>Beyond the screen, I&amp;rsquo;m a karate instructor and an open-source enthusiast. Whether it&amp;rsquo;s enhancing AI frameworks
or tinkering efficient code, I&amp;rsquo;m all about making a difference. When I&amp;rsquo;m looking to
unwind, I enjoy exploring culinary arts and capturing life’s moments through my lens—usually involving scenes
from nature or candid shots of daily life.&lt;/p></description><content>
&lt;img src="https://blog.narain.dev/images/narain1.jpg" alt="Hello Friend" class="center" style="border-radius: 8px; width: 300px; height: auto;" />
&lt;p>Hello! I’m Narain. I tinker with machine learning models for a living, diving into the nuts and bolts of deep learning,
GPU acceleration, and cloud infrastructures. When I&amp;rsquo;m not busy breaking stuff, I might be pushing the boundaries
of what should (or shouldn’t) be done in C++.&lt;/p>
&lt;p>Beyond the screen, I&amp;rsquo;m a karate instructor and an open-source enthusiast. Whether it&amp;rsquo;s enhancing AI frameworks
or tinkering efficient code, I&amp;rsquo;m all about making a difference. When I&amp;rsquo;m looking to
unwind, I enjoy exploring culinary arts and capturing life’s moments through my lens—usually involving scenes
from nature or candid shots of daily life.&lt;/p></content></item><item><title>Distributed_dl</title><link>https://blog.narain.dev/posts/distributed_dl/</link><pubDate>Thu, 21 Mar 2024 16:24:23 -0700</pubDate><guid>https://blog.narain.dev/posts/distributed_dl/</guid><description>&lt;h1 id="distributed-deep-learning-training">Distributed Deep learning training&lt;/h1>
&lt;h2 id="training-routine">Training routine&lt;/h2>
&lt;h2 id="data-parallel">Data Parallel&lt;/h2>
&lt;h2 id="model-parallelism">model parallelism&lt;/h2>
&lt;h2 id="pipeline-parallelism">pipeline parallelism&lt;/h2>
&lt;h2 id="tensor-parallelism">Tensor parallelism&lt;/h2>
&lt;h2 id="deepspeed">Deepspeed&lt;/h2>
&lt;h2 id="deepspeed-zero">Deepspeed zero&lt;/h2></description><content>&lt;h1 id="distributed-deep-learning-training">Distributed Deep learning training&lt;/h1>
&lt;h2 id="training-routine">Training routine&lt;/h2>
&lt;h2 id="data-parallel">Data Parallel&lt;/h2>
&lt;h2 id="model-parallelism">model parallelism&lt;/h2>
&lt;h2 id="pipeline-parallelism">pipeline parallelism&lt;/h2>
&lt;h2 id="tensor-parallelism">Tensor parallelism&lt;/h2>
&lt;h2 id="deepspeed">Deepspeed&lt;/h2>
&lt;h2 id="deepspeed-zero">Deepspeed zero&lt;/h2></content></item><item><title>Online Softmax</title><link>https://blog.narain.dev/posts/online-softmax/</link><pubDate>Mon, 26 Feb 2024 16:32:46 -0700</pubDate><guid>https://blog.narain.dev/posts/online-softmax/</guid><description>&lt;p>One of the most important task in deep learning is classification. This involves predicting the class to which
a given input data belongs. Models such as convolution neural networks (CNN) and Large language models use classification layers.
These models produce output predictions for all possible classes, but these predictions are not immediately usable as they can be
any floating point number. The softmax function is essential in converting these raw model outputs, known as logits, into
probabilities that sum to one, making them interpretable&lt;/p></description><content>&lt;p>One of the most important task in deep learning is classification. This involves predicting the class to which
a given input data belongs. Models such as convolution neural networks (CNN) and Large language models use classification layers.
These models produce output predictions for all possible classes, but these predictions are not immediately usable as they can be
any floating point number. The softmax function is essential in converting these raw model outputs, known as logits, into
probabilities that sum to one, making them interpretable&lt;/p>
&lt;h2 id="the-role-of-softmax-in-classification">The role of Softmax in Classification&lt;/h2>
&lt;h3 id="why-softmax">Why softmax?&lt;/h3>
&lt;p>Raw model outputs, or logits can lie anywhere within the floating-point number range, making them difficult to interpret. The
Softmax function addresses this by normalizing the logits into a probability distribution. This allows us to understand the model&amp;rsquo;s
confidence in its predictions.&lt;/p>
&lt;h2 id="steps-to-calculate-sofmax">Steps to calculate Sofmax&lt;/h2>
&lt;figure class="center" >
&lt;img src="https://blog.narain.dev/images/softmax_default.png" alt="default softmax function" style="border-radius: 8px;" />
&lt;figcaption class="center" >Simple Softmax implementation&lt;/figcaption>
&lt;/figure>
&lt;ol>
&lt;li>
&lt;p>Normalizing by maximum logit:&lt;/p>
&lt;ul>
&lt;li>The first step involves calculating the maximum value among the model&amp;rsquo;s outputs. This helps in normalizing the outputs
and prevents potential overflow issues during exponentiation.&lt;/li>
&lt;/ul>
&lt;/li>
&lt;li>
&lt;p>Exponentiation:&lt;/p>
&lt;ul>
&lt;li>Next we subtract the maximum logit from each logit and calculate the exponent of these normalized values. This step transforms
the logits into a form where they can be compared proportionally.&lt;/li>
&lt;/ul>
&lt;/li>
&lt;li>
&lt;p>Division by sum of exponents:&lt;/p>
&lt;ul>
&lt;li>Finally we divide each exponentiated value by the sum of all exponentiated values. This step ensures that the resulting
probabilities sum to one, providing a valid probability distribution.&lt;/li>
&lt;/ul>
&lt;/li>
&lt;/ol>
&lt;div class="collapsable-code">
&lt;input id="1" type="checkbox" checked />
&lt;label for="1">
&lt;span class="collapsable-code__language">python&lt;/span>
&lt;span class="collapsable-code__title">Softmax implementation pytorch&lt;/span>
&lt;span class="collapsable-code__toggle" data-label-expand="show" data-label-collapse="hide">&lt;/span>
&lt;/label>
&lt;pre class="language-python" >&lt;code>
import torch
def softmax(x):
&amp;#34;&amp;#34;&amp;#34;Compute softmax values for each set of scores in x.&amp;#34;&amp;#34;&amp;#34;
if not isinstance(x, torch.Tensor):
x = torch.tensor(x, dtype=torch.float32)
x_max = x.max(dim=-1, keepdim=True).values
e_x = torch.exp(x - x_max)
return e_x / e_x.sum(dim=-1, keepdim=True)
# Example usage
input_tensor = torch.tensor([1.0, 2.0, 3.0])
softmax_output = softmax(input_tensor)
print(softmax_output)
&lt;/code>&lt;/pre>
&lt;/div>
&lt;p>As we can see, the standard Softmax algorithm has some drawbacks. One significant issue is that it requires multiple passes
over the entire tensor to calculate the softmax, makind it less cache-friendly and potentially inefficient.&lt;/p>
&lt;h2 id="introducing-online-softmax">Introducing Online Softmax&lt;/h2>
&lt;p>To address these inefficiencies, we can explore an alternative approach known as online softmax. This method aims to
improve computational efficiency and cache performance by processing the data in a more streamlined manner.&lt;/p>
&lt;figure class="center" >
&lt;img src="https://blog.narain.dev/images/softmax-online.png" alt="Online softmax" style="border-radius: 8px;" />
&lt;figcaption class="center" >Online Softmax&lt;/figcaption>
&lt;/figure>
&lt;ol>
&lt;li>
&lt;p>Single pass for maximum and sum calculation:&lt;/p>
&lt;ul>
&lt;li>Instead of first finding the maximum value in one pass and then computing the sum of exponentials in another,
this code combines these operation. When a new maximum value is found the sum is adjusted accordingly, ensuring
accuracy without additional passes.&lt;/li>
&lt;/ul>
&lt;/li>
&lt;li>
&lt;p>Numerical Stability&lt;/p>
&lt;ul>
&lt;li>By subtracting the maximum value &lt;code>maxval&lt;/code> from each element before exponentiation, the code prevents potential overflow
issues that could occur with large input values.&lt;/li>
&lt;/ul>
&lt;/li>
&lt;li>
&lt;p>Efficiency:&lt;/p>
&lt;ul>
&lt;li>This approach is more cache-friendly as it reduces the number of passes over the data, thus maximising the number of times the
data needs to be fetched from memory.&lt;/li>
&lt;/ul>
&lt;/li>
&lt;/ol>
&lt;div class="collapsable-code">
&lt;input id="2" type="checkbox" checked />
&lt;label for="2">
&lt;span class="collapsable-code__language">C&lt;/span>
&lt;span class="collapsable-code__title">Online Softmax implementation&lt;/span>
&lt;span class="collapsable-code__toggle" data-label-expand="show" data-label-collapse="hide">&lt;/span>
&lt;/label>
&lt;pre class="language-C" >&lt;code>
{#include &amp;lt;math.h&amp;gt;
#include &amp;lt;float.h&amp;gt; // For FLT_MAX
void softmax_online(const float *input, float *output, int n) {
float max_val = -INFINITY;
float sum = 0.0f;
for (int i = 0; i &amp;lt; n; i&amp;#43;&amp;#43;) {
float maxval_prev = max_val;
if (inp_row[j] &amp;gt; max_val) {
max_val = inp_row[j];
sum = sum &amp;#43; expf(maxval_prev - max_val) &amp;#43; expf(inp_row[j] - max_val);
} else {
sum &amp;#43;= expf(inp_row[j] - max_val);
}
}
for (int i=0; i&amp;lt;n; i&amp;#43;&amp;#43;) {
output[i] = expf(inp_row[i] - max_val) / sum;
}
}
&lt;/code>&lt;/pre>
&lt;/div>
&lt;div class="collapsable-code">
&lt;input id="3" type="checkbox" checked />
&lt;label for="3">
&lt;span class="collapsable-code__language">C&lt;/span>
&lt;span class="collapsable-code__title">Online Softmax implementation CUDA&lt;/span>
&lt;span class="collapsable-code__toggle" data-label-expand="show" data-label-collapse="hide">&lt;/span>
&lt;/label>
&lt;pre class="language-C" >&lt;code>
template &amp;lt;typename T&amp;gt;
__global__
void softmax_kernel_v2(T* qk_buf_, /*const T* attr_mask, */const int batch_size, const int head_num,
const int seq_len, const T scaler)
{
// int batch_id = blockIdx.x / head_num / seq_len;
// int seq_id = blockIdx.x % seq_len;
int qk_offset = blockIdx.x * seq_len;
// int mask_offset = batch_id * seq_len * seq_len &amp;#43; seq_id * seq_len;
__shared__ float s_sum, s_max;
float qk = threadIdx.x &amp;lt; seq_len ? (float)qk_buf_[threadIdx.x &amp;#43; qk_offset] : 0.0f;
// float mask_val = threadIdx.x &amp;lt; seq_len ? (float)attr_mask[threadIdx.x &amp;#43; mask_offset] : 0.0f;
// mask_val = (1.0f - mask_val) * -10000.0f;
// float tmp = threadIdx.x &amp;lt; seq_len ? (float)(qk * (float)scaler &amp;#43; mask_val) : -1e20f;
float tmp = -1e20f;
float max_val = blockReduceMax&amp;lt;float&amp;gt;(tmp);
if(threadIdx.x == 0)
s_max = max_val;
__syncthreads();
float qk_tmp = threadIdx.x &amp;lt; seq_len ? __expf((float)(tmp - s_max)) : 0.0f;
float sum_val = blockReduceSum&amp;lt;float&amp;gt;(qk_tmp);
if(threadIdx.x == 0)
{
s_sum = sum_val &amp;#43; 1e-6f;
}
__syncthreads();
if(threadIdx.x &amp;lt; seq_len)
qk_buf_[threadIdx.x &amp;#43; qk_offset] = (T)(qk_tmp / s_sum);
}
&lt;/code>&lt;/pre>
&lt;/div>
&lt;p>This code becomes even more efficient when used on GPU&amp;rsquo;s as all tensor data can be moved to GPU&amp;rsquo;s shared memory&lt;/p>
&lt;h1 id="references">References&lt;/h1>
&lt;ul>
&lt;li>&lt;a href="https://arxiv.org/pdf/1805.02867">Online normalizer calculation for softmax Paper&lt;/a>&lt;/li>
&lt;/ul></content></item><item><title>Sentencepiece Tokenizer</title><link>https://blog.narain.dev/posts/sentencepiece-tokenizer/</link><pubDate>Mon, 15 Jan 2024 04:56:12 -0700</pubDate><guid>https://blog.narain.dev/posts/sentencepiece-tokenizer/</guid><description>&lt;h1 id="sentencepiece-tokenizer">Sentencepiece tokenizer&lt;/h1>
&lt;h2 id="introduction">Introduction&lt;/h2>
&lt;p>In the rapidly evolving field of natural language processing (NLP), the efficiency and accuracy of language models
hinge significantly on how text data is prepared and processed before training. At the heart of this preparation is
the process of tokenization, a crucial step where raw text is transformed into a structured format that the models can
interpret from. Among the variety of tokenization methods, the sentencepiece tokenizer stands out as a versatile tool
that efficiently handles diverse languages without relying on predefined word boundaries.&lt;/p></description><content>&lt;h1 id="sentencepiece-tokenizer">Sentencepiece tokenizer&lt;/h1>
&lt;h2 id="introduction">Introduction&lt;/h2>
&lt;p>In the rapidly evolving field of natural language processing (NLP), the efficiency and accuracy of language models
hinge significantly on how text data is prepared and processed before training. At the heart of this preparation is
the process of tokenization, a crucial step where raw text is transformed into a structured format that the models can
interpret from. Among the variety of tokenization methods, the sentencepiece tokenizer stands out as a versatile tool
that efficiently handles diverse languages without relying on predefined word boundaries.&lt;/p>
&lt;p>Tokenization not only facilitates the understanding of language nuances by models but also plays a pivotal role
in compressing textual inputs. This compression is essential, as it significantly reduces the dimensionality and complexity
of the data, enabling quicker processing and more effective learning. SentencePiece, in particular employs a subword
tokenization approach this is adept at managing vocabularies in a way that balances the granularity and the breadth of
linguistic elements.&lt;/p>
&lt;p>We will focus on the intricacies of tokenizers, with a focus on SentencePiece, exploring their indespensible role in the
preprocessing of data for language models. We will discuss the general process of preprocessing, normalizing and
post-processing involved in tokenization, shedding light on how these steps contribute to the robuest performance
of language models.&lt;/p>
&lt;h2 id="understanding-tokenizers">Understanding Tokenizers&lt;/h2>
&lt;p>Tokenizers are the first point of interaction with text data in any nlp system. They transform the raw text into tokens,
which are smaller pieces that can be more easily digested by language models. These tokenizers are essentially the building
blocks of text analysis and model training. There are several types of tokenizers, each suited to different tasks and languages.
Let&amp;rsquo;s explore the main types and provide examples for each.&lt;/p>
&lt;h3 id="preprocessing">Preprocessing&lt;/h3>
&lt;p>preprocessing is a critical initial step in the text handling pipeline, aimed at preparing and cleaning the text data
before it undergoes tokenization and further analysis. The main goal of preprocessing is to standardize the text to
reduce variability that is&amp;rsquo;nt relavent for the subsequent process or analyses. This step ensures that the input to the
model is as clean and uniform as possible, improving the model&amp;rsquo;s ability to learn and make accurate predictions.
Here are some key techniques typically employed during the preprocessing phase.&lt;/p>
&lt;ol>
&lt;li>
&lt;p>Lowercasing: Converting all characters in the text to lowercase helps in standardizing the data. This means that words like
House and house are treated the same preventing the model from treating them as different tokens unnecassarily.&lt;/p>
&lt;/li>
&lt;li>
&lt;p>Removing special Characters and punctuation: Text often contains various punctuation marks and special characters, which
may not be necassary for many NLP tasks. Removing these elements helps focus the model on the content of words rather than formatting.&lt;/p>
&lt;/li>
&lt;li>
&lt;p>Expanding Contractions: In english and many other languages, contractions such as &amp;ldquo;Can&amp;rsquo;t&amp;rdquo;, &amp;ldquo;don&amp;rsquo;t&amp;rdquo;. and &amp;ldquo;it&amp;rsquo;s&amp;rdquo; are common.
Expanding these to &amp;ldquo;cannot&amp;rdquo; &amp;ldquo;do not.&amp;rdquo; and &amp;ldquo;it is&amp;rdquo; helps maintain consistency in verb forms and reduces ambiguity in parsing the text.&lt;/p>
&lt;/li>
&lt;li>
&lt;p>Removing stopwords: common words such as &amp;ldquo;and&amp;rdquo;, &amp;ldquo;is&amp;rdquo;, and &amp;ldquo;but&amp;rdquo; can be filtered out during preprocessing. These words are usually frequent
and carry less meaningful content for many NLP tasks, such as sentiment analysis or topic modelling.&lt;/p>
&lt;/li>
&lt;li>
&lt;p>Trimming spaces: Excess whitespaces, including spaces, tabs and new lines can be normalized by trimming them to a single space or removing
them entirely, which helps in maintaining consistency in text format&lt;/p>
&lt;/li>
&lt;/ol>
&lt;p>The above text format are not specifically related to sentencepiece model but are some of the common techniques used for preprocessing.&lt;/p>
&lt;h3 id="normalizing">Normalizing&lt;/h3>
&lt;p>Normalization deals with the way text is represented to ensure consistency. it is particularly important when dealing with different scripts
or when the input data comes from various sources that might format text differently.&lt;/p>
&lt;ol>
&lt;li>
&lt;p>Unicode Normalization: Converts text to a consistent unicode format, resolving issues like different character encodings for the same
characters. Used by BPE, SentencePiece&lt;/p>
&lt;/li>
&lt;li>
&lt;p>Stemming and lemmatization: reduces words to their base or root form, either through cutting off inflections (stemming) or by
using lexical knowledge of the language (lemmatization).&lt;/p>
&lt;/li>
&lt;/ol>
&lt;h2 id="byte-pair-encoding-bpe">Byte Pair Encoding (BPE)&lt;/h2>
&lt;p>BPE is another popular subword tokenization method originally developed for data compression. BPE iteratively merges the most frequent pairs
of bytes (or characters) in a dataset to form new tokens, which effectively reduces the vocabulary size and handles the rare words more efficiently.&lt;/p>
&lt;ol>
&lt;li>
&lt;p>Vocabulary Independence: While both methods are effective at creating subword vocabularies, SentencePiece does not rely on
pre-tokenized text, making it more flexible in handling raw text inputs. BPE typically starts with a base vocabulary of individual
characters and builds up by merging frequent pairs.&lt;/p>
&lt;/li>
&lt;li>
&lt;p>Handling of Rare Words: Both SentencePiece and BPE help mitigate the issue of rare words through the use of subword units.
However, SentencePiece&amp;rsquo;s algorithm allows for more direct control over the tokenization granularity, which can be advantageous
in balancing the vocabulary size against model performance.&lt;/p>
&lt;/li>
&lt;li>
&lt;p>Ease of Use: SentencePiece provides an easy-to-use implementation that integrates seamlessly with major machine learning
frameworks and supports direct text inputs. BPE often requires initial text processing and vocabulary building,
which might add complexity to the pipeline.&lt;/p>
&lt;/li>
&lt;li>
&lt;p>Performance in Multilingual Settings: SentencePiece is particularly well-suited for multilingual environments
because it treats the text as a sequence of raw Unicode characters, which naturally accommodates multiple languages
without bias towards any particular language&amp;rsquo;s syntax or morphology.&lt;/p>
&lt;/li>
&lt;/ol>
&lt;h3 id="example">Example&lt;/h3>
&lt;ul>
&lt;li>Initial Text&lt;/li>
&lt;/ul>
&lt;blockquote>
&lt;p>ABABCABCD&lt;/p>&lt;/blockquote>
&lt;ul>
&lt;li>Count frequency of pairs&lt;/li>
&lt;/ul>
&lt;blockquote>
&lt;p>AB: 3
BA: 1
BC: 2
CA: 1
CD: 1&lt;/p>&lt;/blockquote>
&lt;ul>
&lt;li>Most frequent pair&lt;/li>
&lt;/ul>
&lt;blockquote>
&lt;p>AB&lt;/p>&lt;/blockquote>
&lt;ul>
&lt;li>Replace &lt;code>ab&lt;/code> with a new symbol say `x'&lt;/li>
&lt;/ul>
&lt;blockquote>
&lt;p>New text: XAXCXCD&lt;/p>&lt;/blockquote>
&lt;ul>
&lt;li>Count the frequency of pairs in new text:&lt;/li>
&lt;/ul>
&lt;blockquote>
&lt;p>XA: 1
AX: 1
XC: 2
CX: 1
CD: 1&lt;/p>&lt;/blockquote>
&lt;ul>
&lt;li>Most frequent pair:&lt;/li>
&lt;/ul>
&lt;blockquote>
&lt;p>XC&lt;/p>&lt;/blockquote>
&lt;ul>
&lt;li>Repeat the same process until the desired number of tokens is achieved&lt;/li>
&lt;/ul>
&lt;h2 id="sentencepiece-tokenizer-1">SentencePiece Tokenizer&lt;/h2>
&lt;p>The SentencePiece tokenizer is a robust and flexible tool designed to efficiently manage the tokenization of
text without the need for pre-defined word boundaries. This makes it particularly suitable for languages where whitespace
is not a reliable delimiter. Unlike traditional tokenization methods that rely on whitespace and pre-defined vocabularies,
SentencePiece treats the input text as a raw stream of Unicode characters and learns a vocabulary of subword units directly from this text.&lt;/p>
&lt;ol>
&lt;li>
&lt;p>Language Agnosticism: SentencePiece is designed to be independent of the language being processed. It works
effectively across various languages, including those that do not use spaces to separate words.&lt;/p>
&lt;/li>
&lt;li>
&lt;p>Subword Tokenization: It utilizes subword units, which can effectively capture common prefixes, suffixes, and
roots, reducing the out-of-vocabulary issue and preserving meaningful linguistic units.&lt;/p>
&lt;/li>
&lt;li>
&lt;p>End-to-End Tokenization: SentencePiece handles both the tokenization and detokenization processes, ensuring that
the original text can be perfectly reconstructed from the tokens.&lt;/p>
&lt;/li>
&lt;/ol>
&lt;h3 id="example-1">Example&lt;/h3>
&lt;ul>
&lt;li>Initial text&lt;/li>
&lt;/ul>
&lt;blockquote>
&lt;p>this is a simple example&lt;/p>&lt;/blockquote>
&lt;ul>
&lt;li>segment the text into characters&lt;/li>
&lt;/ul>
&lt;blockquote>
&lt;p>t h i s _ i s _ a _ s i m p l e _ e x a m p l e&lt;/p>&lt;/blockquote>
&lt;ul>
&lt;li>
&lt;p>count the frequency of each character and pair of characters, then merge the most frequent pairs iteratively&lt;/p>
&lt;/li>
&lt;li>
&lt;p>assume the vocabulary learned contains&lt;/p>
&lt;/li>
&lt;/ul>
&lt;blockquote>
&lt;p>{&amp;rsquo;th&amp;rsquo;, &amp;lsquo;is&amp;rsquo;, &amp;lsquo;_&amp;rsquo;, &amp;lsquo;a&amp;rsquo;, &amp;lsquo;si&amp;rsquo;, &amp;lsquo;mple&amp;rsquo;, &amp;rsquo;ex&amp;rsquo;, &amp;lsquo;amp&amp;rsquo;, &amp;rsquo;le&amp;rsquo;}&lt;/p>&lt;/blockquote>
&lt;ul>
&lt;li>Tokenize the text using learned vocabulary&lt;/li>
&lt;/ul>
&lt;blockquote>
&lt;p>Tokenized text: [th, is, _, is, _, a, _, si, mple, _, ex, amp, le]&lt;/p>&lt;/blockquote></content></item><item><title>Why Audio Files Are Really Hard to Compress</title><link>https://blog.narain.dev/posts/compression1/</link><pubDate>Fri, 24 Feb 2023 03:01:20 -0700</pubDate><guid>https://blog.narain.dev/posts/compression1/</guid><description>&lt;p>Audio data is inherently complex and dense. Unlike text, where redundancy is common (think repeated words or phrases), audio
signals are continuous streams of varying frequencies and amplitudes. These signals can include everything from human speech
and music to ambient noises and complex soundscapes. The richness and variety in audio data make it challenging to identify
and eliminate redundancy without losing essential information.&lt;/p>
&lt;h2 id="human-perception-of-sound">Human perception of sound&lt;/h2>
&lt;p>Human ears are sensitive to a wide range of frequencies, from about 20 Hz to 20 kHz. Our perception of sound is also
influenced by various factors, such as pitch, volume, and timbre. Any compression algorithm must take these perceptual
aspects into account to ensure that the compressed audio still sounds natural to human listeners. This requirement adds
an extra layer of complexity to the compression process.&lt;/p></description><content>&lt;p>Audio data is inherently complex and dense. Unlike text, where redundancy is common (think repeated words or phrases), audio
signals are continuous streams of varying frequencies and amplitudes. These signals can include everything from human speech
and music to ambient noises and complex soundscapes. The richness and variety in audio data make it challenging to identify
and eliminate redundancy without losing essential information.&lt;/p>
&lt;h2 id="human-perception-of-sound">Human perception of sound&lt;/h2>
&lt;p>Human ears are sensitive to a wide range of frequencies, from about 20 Hz to 20 kHz. Our perception of sound is also
influenced by various factors, such as pitch, volume, and timbre. Any compression algorithm must take these perceptual
aspects into account to ensure that the compressed audio still sounds natural to human listeners. This requirement adds
an extra layer of complexity to the compression process.&lt;/p>
&lt;h2 id="ineffictive-ways-we-visualize-waveforms">Ineffictive ways we visualize waveforms&lt;/h2>
&lt;p>Due to The huge amount of data contained in waveforms we try to interpret audio using different techniques like waveforms
spectograms these help us get heatmaps of frequency and amplitude distributions over a period of time.&lt;/p>
&lt;h2 id="types-of-audio-compression">Types of audio compression&lt;/h2>
&lt;h3 id="lossless-compression">Lossless Compression:&lt;/h3>
&lt;p>Techniques like FLAC (Free Lossless Audio Codec) preserve the exact original audio data, making it possible to reconstruct
the original audio perfectly. However, the compression ratios achieved are relatively modest, typically reducing file sizes by about 50% at best.&lt;/p>
&lt;h3 id="lossy-compression">Lossy Compression:&lt;/h3>
&lt;p>Formats like MP3 and AAC use psychoacoustic models to remove sounds that are less perceivable to human ears. While
these methods achieve higher compression ratios, they do so at the cost of some loss in audio quality. The
challenge lies in balancing compression efficiency with audio fidelity.&lt;/p>
&lt;h3 id="applications-in-audio-compression">Applications in Audio Compression&lt;/h3>
&lt;h4 id="mp3-encoding">MP3 Encoding:&lt;/h4>
&lt;p>MP3 compression uses the Modified Discrete Cosine Transform (MDCT), a variation of the Fourier Transform, to convert
audio signals into the frequency domain. It then applies psychoacoustic models to remove inaudible frequencies and quantizes the remaining
data to achieve compression.&lt;/p>
&lt;h4 id="spectral-analysis">Spectral Analysis:&lt;/h4>
&lt;p>Fourier Transform is also used in various spectral analysis techniques, which help in identifying and compressing the most important
components of the audio signal while discarding redundant or less important parts.&lt;/p></content></item></channel></rss>