Introduction to Mojo: The Programming Language for AI Which Is 35000x Faster Than Python

Gursimar Singh
Coinmonks

--

With AI (Artificial Intelligence) on the rise, we need appropriate tools to build efficiently. In this article, we’ll be looking at the introduction and how to get started with the new revolutionary programming language, Mojo. We’ll also look at the code for programs such as binary search in Mojo and compare that with Python.

Let’s start with the basic history.

History & Introduction

Mojo was created by Chris Lattner, the creator of the Swift programming language and the LLVM Compiler Infrastructure. Lattner started working on Mojo in 2019, and the language was first released in May 2023.

Lattner created Mojo because he was dissatisfied with the performance of Python for AI and ML applications. Python is a powerful and versatile language, but it is not as fast as some other languages, such as C and C++. Lattner wanted to create a language that would combine the usability of Python with the speed of C and C++.

Mojo is designed to be a superset of Python. This means that Mojo code can be written in a way that is compatible with Python code. However, Mojo also adds a number of features that make it more powerful and efficient.

Mojo is still under development, but it has already gained some popularity. It is a promising new language that could become a major player in the AI and ML space. It is a powerful and versatile language that can be used to create high-performance models that are easy to develop and maintain. As Mojo continues to develop, it is likely to become even more popular and widely used.

Why Python?

Because it is dominantly used in AI and many other domains. Plus, it’s easy to learn and has a fantastic community and numerous libraries.

Problems with Python?

In a nutshell? It is single-threaded and slow.

Mojo is based on Python, but it adds a number of features that make it more powerful and efficient. These features include:

  • Static typing: Static typing can help to prevent errors by ensuring that the types of variables and expressions are known at compile time. This can also help improve performance by allowing the compiler to optimize code more aggressively.
  • Automatic memory management: Automatic memory management can help simplify code by eliminating the need for programmers to allocate and free memory explicitly. This can also help improve performance by reducing the amount of time spent managing memory.
  • SIMD support: SIMD support can be used to improve the performance of code that operates on large amounts of data. SIMD instructions allow the CPU to perform multiple operations on the same data at the same time, which can significantly speed up execution.
  • Concurrency support: Concurrency support can be used to improve the performance of code that performs a lot of parallel work. Concurrency allows multiple tasks to be executed at the same time, which can help to reduce the overall execution time.
  • Ease of use: Mojo is designed to be easy to use, even for programmers who are new to programming. It has a simple syntax that is similar to Python, and it provides a number of features that make it easy to develop and maintain code.

Mojo Lang is a programming language designed for AI hardware, such as GPUs with CUDA support. It accomplishes this through the use of Multi-Level Intermediate Representation (MLIR) to scale hardware varieties without complexity.

Since Mojo Lang is a superset of Python, learning a new programming language is not required. Isn’t that so? The base language is entirely compatible with Python and enables interaction with the Python ecosystem and the use of Python libraries such as Matplotlib and NumPy.

Additional features?

  • Built-in struct keyword, which is similar to a Python class. However,, struct is static, whereas class is dynamic.
  • With Mojo Lang’s parallelize function, your code can be multithreaded, which can increase performance by a factor of 2000.
  • It includes an integrated tiling optimization utility that enables you to cache and utilize your data more efficiently.
  • It enables you to autotune the code in order to discover the optimal parameters for the target hardware.

Mojo promises to deliver astounding speed, up to 35,000 times faster than conventional Python.

We will dive into Mojo and explore its potential. But before we begin, here are five key aspects you should be aware of:

  1. Mojo is not a random side project. It’s the brainchild of Chris Lattner, the creator of the Swift programming language and the LLVM compiler toolchain.
  2. Mojo is a language designed with AI hardware in mind. It’s adept at leveraging multi-level intermediate representation to scale to exotic hardware types without complexity. It also boasts built-in auto-tuning to optimize your code for your target hardware.
  3. Mojo is a superset of Python, just as TypeScript is a superset of JavaScript. This means you don’t have to learn a new language to harness its power. It does come with a host of features that go beyond Python, such as VAR and LET declarations and structs. However, the base language is fully compatible with Python, enabling interoperability with the Python ecosystem.
  4. Mojo adds strong type-checking to Python’s capabilities. While you can still use dynamic types, static types are crucial for optimized performance and error checking.
  5. For memory management, Mojo uses an ownership system and borrow checkers akin to Rust. It also supports manual memory management with pointers, like C++.

Please note that, as of now, Mojo is in early development and not available to the public. However, there is a waitlist to try it out.

You can write Mojo code in a file ending with .mojo or even .fire (emoji), which is a fun perk not available in Python. It can also run in a Jupyter notebook, functioning like an interpreted language.

Mojo is a new LLVM programming language.

What is LLVM?

Do you wish to create your own programming language? LLVM is an instrument for creating and optimising compilers that serves as the foundation for many programming languages, including Rust, Swift, CUDA, C, and C++.

LLVM is a comprehensive toolkit designed to construct and refine compilers. Building a programming language from scratch is no easy feat. It requires bridging the gap between human-friendly syntax and machine-ready code that can run on a variety of architectures. Born in 2003 from the efforts of Chris Lattner, then a grad student at the University of Illinois, LLVM streamlines the intricate process of transforming source code into machine code. Today, it forms the backbone of compilers like Clang for C and C++, and powers languages such as Rust, Swift, Julia, and many others.

One of LLVM’s most significant features is its ability to represent high-level source code in a language-agnostic format, known as Intermediate Representation (IR). This innovative feature allows diverse languages like CUDA and Ruby to generate identical IR, enabling them to utilize shared tools for analysis and optimization before their conversion into machine code tailored to a specific chip architecture.

A compiler can essentially be divided into three parts: the front end, the middle end, and the back end. The front end translates the source code text into IR, while the middle end scrutinizes and optimizes the generated code. Lastly, the back end translates the IR into native machine code.

To craft your own programming language from scratch, you would start by installing LLVM and creating a C file. Once you have a vision for your programming language syntax, you’ll need to develop a lexer that scans the raw source code and breaks it down into tokens like literals, identifiers, keywords, operators, etc.

Following this, you’ll define an Abstract Syntax Tree (AST) to represent the actual structure of the code, showing how different tokens interact. This is achieved by assigning each node its own class. Next, you’ll create a parser to iterate over each token and build the AST.

Once you’ve navigated this complex phase, you’re ready to leverage LLVM primitives to generate the IR. Each type in the AST is given a method named ‘codegen’, which always returns an LLVM value object. These value objects, used to represent single assignment registers, are unique in that they can only be assigned once.

What’s fascinating about these IR primitives is their independence from any specific machine architecture, which makes life much easier for language developers. They no longer need to worry about matching the output to a processor’s instruction set.

With the front end now capable of generating IR, the op tool can be employed to analyze and optimize the resulting code. It performs multiple passes over the IR, eliminating dead code and performing scalar replacement of aggregates.

Finally, it’s time for the back end, where you’ll write a module that takes the IR as input and produces object code that can run on any architecture.

You’ve learned how to build a programming language in a nutshell. Congratulations.

Syntax

A “Hello World” program in Mojo is a replica of that in Python:

print("Hello Mojo!")

let and var declarations

These values use lexical scoping and support name shadowing:

def let_var(x, by):
let r = p
# r= q # error: c is immutable

if r!= q:
let s = q
print(s)

var_let(20, 30)

String

Index and Slicing

from String import String
let s : String = "hello world"
print(s[0])
print(s[:5])
print(s[::2])
print(String("hello world")[0])

# The following code doesn’t work because `StringRef` cannot be subscripted!
let s = "hello world" # Here s is a `StringRef`
print(s[:5])
print("hello world"[0])

Type casting

Cast SIMD

from DType import DType
let x : UI8 = 42 # alias UI8 = SIMD[DType.ui8, 1]
let y : SI8 = x.cast[DType.si8]()

Cast SIMD to Int

let x : UI8 = 42
let y : Int = x.to_int()

Interact with Python

Execute Python code directly in the playground

%%python
print("The number is", 36)

Invoke Python interpreter from Mojo

let x: Int = 36

from PythonInterface import Python
let py = Python.import_module("builtins")

py.print("The number is", x)

Pointer

Create a Pointer

var x : Int = 42  # x must be mutable
let xPtr = Pointer[Int].address_of(x)
print(xPtr.load()) # dereference a pointer

Casting type of Pointer

let yPtr : Pointer[UI8] = xPtr.bitcast[UI8]()

Null pointer

Pointer[Int]()
Pointer[Int].get_null()

Interact with C

Call C function using Intrinsics.external_call

external_call[c_function_name, return_type, arg0_type, arg1_type, ...](arg0, arg1, ...) -> return_type
from Intrinsics import external_call
# time_t ts = time(NULL);
let ts = external_call["time", Int, Pointer[Int]](Pointer[Int]())
print(ts)
from Intrinsics import external_call

alias int = SI32
@value
@register_passable("trivial")
struct C_tm:
var tm_sec: int
var tm_min: int
var tm_hour: int
var tm_mday: int
var tm_mon: int
var tm_year: int
var tm_wday: int
var tm_yday: int
var tm_isdst: int

fn __init__() -> Self:
return Self {
tm_sec: 0,
tm_min: 0,
tm_hour: 0,
tm_mday: 0,
tm_mon: 0,
tm_year: 0,
tm_wday: 0,
tm_yday: 0,
tm_isdst: 0
}

# time_t ts
let ts : Int
# time(&ts);
let tsPtr = Pointer[Int].address_of(ts)
external_call["time", Int, Pointer[Int]](tsPtr)
# struct tm *tm = gmtime(&ts)
let tmPtr = external_call["gmtime", Pointer[C_tm], Pointer[Int]](tsPtr)
let tm = tmPtr.load()
print(tm.tm_hour, ":", tm.tm_min, ":", tm.tm_sec)

Built-in Modules

Standard Library Modules

  • Assert: Implements various asserts.
  • Atomic: Implements the Atomic class.
  • Autotune: Provides interfaces to adaptive compilation in Mojo.
  • Benchmark: Implements the Benchmark class for runtime benchmarking.
  • Bit: Provides functions for bit manipulation.
  • Buffer: Implements the Buffer class.
  • Complex: Implements the Complex type.
  • DType: Implements the DType class.
  • Functional: implements higher-order functions.
  • IO: Provides utilities for working with input/output.
  • Index: Implements StaticIntTuple which is commonly used to represent N-D indices.
  • Intrinsics: defines intrinsics.
  • List: Provides utilities for working with static and variadic lists.
  • Math: defines math utilities.
  • Memory: defines functions for memory management.
  • Numerics: defines utilities to work with numeric types.
  • OS: Implements basic routines for working with the OS.
  • Pointer: Implements classes for working with pointers.
  • Polynomial: Provides two implementations for evaluating polynomials.
  • Random: Provides functions for random numbers.
  • Range: Implements a ‘range’ call.
  • Reductions: Implements SIMD reductions.
  • SIMD: Implements the SIMD struct.
  • Sort: Implements sorting functions.
  • StaticTuple: Implements StaticTuple, a staticly-sized uniform container.
  • String: Implements basic object methods for working with strings.
  • TargetInfo: Implements methods for querying the host target info.
  • Testing: Implements various testing utils.
  • Time: Implements basic utils for working with time.
  • TypeUtilities: Implements type utilities.
  • Vector: Defines several vector-like classes.

Examples

Creating a plot with matplotlib

def make_plot(m: Matrix):

plt = Python.import_module("matplotlib.pyplot")

fig = plt.figure(1, [10, 10 * yn // xn], 64)
ax = fig.add_axes([0.0, 0.0, 1.0, 1.0], False, 1)
plt.imshow(image)
plt.show()

make_plot(compute_mandelbrot())

Using Python libraries such as BeautifulSoup

html_doc = """
<html><head><title>The Dormouse's story</title></head>
<body>
<p class="title"><b>The Dormouse's story</b></p>

<p class="story">Once upon a time there were three little sisters; and their names were,
<a href="http://example.com/lacie" class="sister" id="link2">Lacie</a> and
<a href="http://example.com/tillie" class="sister" id="link3">Tillie</a>;
and they lived at the bottom of a well.</p>

"""

from PythonInterface import Python

let bs4 = Python.import_module("bs4")
let builtins = Python.import_module("builtins")
let bprint = builtins.print


let soup = bs4.BeautifulSoup(html_doc)

let result = soup.prettify()

builtins.print(result)

bprint("print end!")

Just importing

from PythonInterface import Python

let requests = Python.import_module("requests")
let builtins = Python.import_module("builtins")
let bp = builtins.print

Calculating the area and circumference of a circle

struct Circle:
var pi: FloatLiteral
var radius: FloatLiteral
var area: FloatLiteral
var circumference: FloatLiteral

def main():
let circle01: circle

circle01.pi = 3.1415
circle01.radius = 20

circle01.area = circle01.pi * (circle01.radius * circle01.radius)
circle01.circumference = 2 * circle01.pi * circle01.radius
print("Area:", circle01.area, "\nCircumference:", circle01.circumference)

main()

Finally, Binary Search

Using python,

%%python
import timeit
from typing import List, Union


SIZE = 1000000
MAX_ITERS = 100
COLLECTION = tuple(i for i in range(SIZE)) # Make it aka at compile-time.


def python_binary_search(element: int, array: List[int]) -> int:
start = 0
stop = len(array) - 1
while start <= stop:
index = (start + stop) // 2
pivot = array[index]
if pivot == element:
return index
elif pivot > element:
stop = index - 1
elif pivot < element:
start = index + 1
return -1


def test_python_binary_search():
_ = python_binary_search(SIZE - 1, COLLECTION)


print(
"Average execution time of func in sec",
timeit.timeit(lambda: test_python_binary_search(), number=MAX_ITERS),
)

Using Mojo

from Benchmark import Benchmark
from Vector import DynamicVector


alias SIZE = 1000000
alias NUM_WARMUP = 0
alias MAX_ITERS = 100


fn mojo_binary_search(element: Int, array: DynamicVector[Int]) -> Int:
var start = 0
var stop = len(array) - 1
while start <= stop:
let index = (start + stop) // 2
let pivot = array[index]
if pivot == element:
return index
elif pivot > element:
stop = index - 1
elif pivot < element:
start = index + 1
return -1


@parameter # statement runs at compile-time.
fn get_collection() -> DynamicVector[Int]:
var v = DynamicVector[Int](SIZE)
for i in range(SIZE):
v.push_back(i)
return v


fn test_mojo_binary_search() -> F64:
fn test_closure():
_ = mojo_binary_search(SIZE - 1, get_collection())
return F64(Benchmark(NUM_WARMUP, MAX_ITERS).run[test_closure]()) / 1e9


print(
"Average execution time of func in sec ",
test_mojo_binary_search(),
)

Encoding a string to UTF-8

from Vector import DynamicVector
from String import String

let vec : DynamicVector[SI8] = String("hello world").buffer

Using Benchmark

from Benchmark import Benchmark

fn bench() -> Int:
fn _iter():
for i in range(10000):
pass

return Benchmark().run[_iter]()

print(bench())

Fibonacci series

from Benchmark import Benchmark

fn fib(n: Int) -> Int:
if n < 2:
return n
else:
return fib(n-1) + fib(n-2)


fn foo() -> None:
let total = 35
var summ = 0
for i in range(total):
summ += fib(i)

let ans = (F32(summ)/total)
print(ans)

def foo_test():
fn test_fn():
_ = foo()

let secs = F64(Benchmark().run[test_fn]())
print(secs)

foo_test()

We have covered a lot of ground to get started easily and slowly move forward. Next, we’ll be covering machine learning programs in Mojo.

FAQs? Let’s cover a few!

Q1: Is Mojo complied or interpreted language?

A: Complied

Q2: Does Mojo have garbage collection?

A: No, Mojo does not have garbage collection. The design goal of Mojo is to maintain small binaries without an attached runtime. Instead, Mojo relies on reference counting and destructors to manage memory and resources effectively. Utilizing reference counting and destructors can be beneficial in managing memory and ensuring efficient resource handling within Mojo.

Q3: Can we subscript a string in Mojo?

A: Yes, in Mojo, you can subscript or access individual characters of a String

Q4: Can Mojo be compiled to an executable? If so, can we assume that a Mojo file can be cross-compiled to any architecture?

A: Yes, Mojo can be compiled to an executable. In general, Mojo supports compilation to various architectures as long as there is an LLVM or MLIR backend available for the target architecture.

Q5: What is the naming convention for Mojo language, similar to PEP8 for Python? What is the Zen of Mojo?

A: In Mojo, as a member of the Python family, it is recommended to follow the naming convention specified in PEP8. Adhering to PEP8 guidelines for naming in Mojo is considered the appropriate practice.

Q6: How do I check the currently installed and available packages in the playground?

A: There are almost 200 packages in the playground currently. Many of those are dependencies. So, there are around 50 high-level packages. However, in the future, you’ll be able to install any Python package and use it.

import pkg_resources

installed_packages = [pkg.key for pkg in pkg_resources.working_set]
sorted_packages = print(sorted_packages)

Q7: Does Mojo provide complete support for concurrency?

A: Absolutely. Mojo indeed offers comprehensive concurrency support and a high-performance runtime. However, it’s worth noting that while the underlying capabilities are available, they may not be fully exposed through the current set of APIs.

Q8: Can we assign a Null value to any type in Mojo?

A: According to the roadmap documentation, Mojo currently lacks an Optional type and proper universal nullability. While this is an important feature request, it requires the development of appropriate features before it can be implemented.

Q: Can we perform base64 encoding on strings?

A: Yes

Bonus

Using ‘gmtime' from C

gmtime in C is a function that helps convert a number representing time into a human-readable format, specifically the date and time in Coordinated Universal Time (UTC).

alias int = SI32
@value
@register_passable("trivial")
struct C_tm:
var tm_sec: int
var tm_min: int
var tm_hour: int
var tm_mday: int
var tm_mon: int
var tm_year: int
var tm_wday: int
var tm_yday: int
var tm_isdst: int

fn __init__() -> Self:
return Self {
tm_sec: 0,
tm_min: 0,
tm_hour: 0,
tm_mday: 0,
tm_mon: 0,
tm_year: 0,
tm_wday: 0,
tm_yday: 0,
tm_isdst: 0
}

let rawTime : Int
let rawTimePtr = Pointer[Int].address_of(rawTime)
__mlir_op.`pop.external_call`[
func : "time".value,
_type: None,
](rawTimePtr.address)

let tm = __mlir_op.`pop.external_call`[
func : "gmtime".value,
_type: Pointer[C_tm],
](rawTimePtr).load()

print(tm.tm_hour, ":", tm.tm_min, ":", tm.tm_sec)

Star this repo for more programs written in Mojo: https://github.com/gursimarsm/Mojo-Programming

Conclusion

With AI on the rise, Mojo is a revolutionary innovation in AI and machine learning, addressing the speed limitations of Python while delivering unmatched performance, as stated in the conclusion. By combining Python’s accessibility with C’s strength, Mojo enables developers to unleash the full potential of AI hardware. Mojo has the potential to revolutionize AI and machine learning development due to its compatibility with the Python ecosystem and numerous promising features. Early adoption of Mojo can place developers at the vanguard of this innovative field.

We’ve finally come to the end of this article. I hope you’ve enjoyed it and have learned something new.

I’m always open to suggestions and discussions on LinkedIn. Hit me up with direct messages.

If you’ve enjoyed my writing and want to keep me motivated, consider leaving starts on GitHub and endorse me for relevant skills on LinkedIn.

Until the next one, stay safe and keep learning.

--

--

Gursimar Singh
Coinmonks

Google Developers Educator | Speaker | Consultant | Author @ freeCodeCamp | DevOps | Cloud Computing | Data Science and more