75 comments

[ 0.25 ms ] story [ 205 ms ] thread
All the stuff that requires speed is written in a language that compiles directly to machine code while the machine learning libraries are all python based. That seems standard, no?
I think that happens, but I don't know about standard. It is pretty obvious and natural. However, a lot of code is written in Python and it can be hard to move ML teams to use other tools. Many of them aren't great at programming because their background is stats/math so it can be hard to move critical code to more performant solutions without resistance from the teams.
All the popular machine learning libraries are a Python API to more conveniently communicate with the CUDA code that actually does the calculations on GPUs. The API can be in any other language, but the choice of that language does not really affect performance much, as the heavy lifting in any "Python machine learning library" does not happen in Python anyway.
Why not Swift? (I think I know the answer).

Concurrency in Swift is not yet a solved problem, but libdispatch is quite workable (although not "elegant, out of the box" per the article).

With the work being done in Swift for TensorFlow [0], I'd imagine in a year or two both the infrastructure and the ML portions of a product like Cortex could be written in a single language.

[0] - https://www.tensorflow.org/swift

Swift is an interesting choice, one we haven't explored in depth. Out of curiosity, have you done any work with Swift for Tensorflow/what has your experience been?
Not him but the company I work for has moved most of our ML pipelines away from Python and over to Swift. We started the move after Google announced support for Swift for TF. Before the move over to Swift, we were rewriting a lot of the data paths in C++ but were generally unhappy with the state of TF for C++ libraries. The move to Swift hasn't been without hiccups, however, but the community has been extremely helpful and it was worth it overall. State of Swift for TF libraries today is quite good.

We looked at Go early on but quickly dismissed it because with the move from Python to Go, it didn't seem like we were getting enough benefits to warrant the amount of work that would be required.

Until Swift stops requiring stuff like import Glibc for basic IO and actually supports Windows, I would consider it an Apple only language.
Tensorflow which is for Deep Learning is a very tiny part of the overall ML requirements.

To do large scale data preparation and engineering which necessitates clustering you would need to reinvent Spark. And then how about for the more common algorithms like boosted trees etc. Those algorithms are provided by Apple for iOS/MacOS but not sure if they exist for everyone else.

> Making all of these overlapping API calls in a performative, reliable way is a challenge.

Pythons asyncio is pretty hard to beat. For non-cpu intensive tasks, I find it a pleasure to work with. Goroutines can still have race conditions.

> Originally, we wrote the CLI in Python, but trying to distribute it across platforms proved to be too difficult

Sure, I get that go can cross-compile. But what makes python hard? Python works on every platform, and distributing is just a "pip install" and "pip install -u" Surely thats easier than "Download the correct binary for the platform, unzip it, change permissions, add it to your path, then do it all over again for every update"

I was the original author of the awseb cli and we found that pip install was significantly less of a hurdle than a go binary and decided to do it in Python instead. If a user on windows has a hard time installing python and pip, telling them to drop a binary and change their path isnt going to be any easier.

Not a fair comparison. Go has go install.
"... is just a "pip install..."

I was yelled at a few times for not using package and environment management tools (Conda, etc). So, when working with Python it is not just a "pip install" anymore

> But what makes python hard?

You ever tried setting up Python on a new machine? https://xkcd.com/1987/

I do it all the time:

  apt install python3 python3-pip python3-virtualenv
Python packaging is one of its worst warts, but the Python runtime itself is very easy to install.
We thought similarly to you about the relative ease of "pip install"—which is why we originally wrote the CLI in Python. Pretty immediately, however, we heard back from users who experienced friction. With the Go binary, we're able to share a one line bash command that users on Mac/Linux (Windows coming soon) can run to install the Cortex CLI, which removed the need for us to instruct users on how to configure their local environments. We've found that this one line install works better for our users: https://www.cortex.dev/install

Also, when we had the Python CLI, some of our users complained that in their CI systems which ran `cortex deploy`, they didn’t need Python/Pip in their images, and installing them was inconvenient

If you are having them bash a shell script, that shell script could install a python package as well. If your users like the bash install, I dont think rewriting in go was required for that. I guess I dont understand why this is any easier. For "cross platform" windows users dont even have bash.
> Python works on every platform, and distributing is just a "pip install" and "pip install -u"

Until you have a dependency which has a C dependency (like a crypto framework, SQL connector, etc). Suddenly you need an entire compiler toolchain, dev dependencies, all the library headers, and a decent amount of time. Also the errors thrown when these compile steps fail are anything but helpful for new users. If you are lucky there is already a wheel for your platform/arch.

> Surely thats easier than "Download the correct binary for the platform, unzip it, change permissions, add it to your path, then do it all over again for every update"

This is trivial to automate using a script and has a ton less failure modes to deal with than Pip would have (do you have the correct Python version?, is there a compiler installed for C modules?, etc).

> Until you have a dependency which has a C dependency (like a crypto framework, SQL connector, etc).

but... that's not a python problem.

every time people say they are having a hard time installing a python module, it's almost always a non-pure python module. it has an extension in c or c++. if that tells us anything, it's that mixing c and c++ makes software hard to install...

a fairer comparison with go here is with a go package that has extensions or bindings written in another language.

Sure, but the most popular Python connectors for MySQL and PostgreSQL both utilize C or C++ extensions, whereas the equivalent connectors for Go are both written in pure Go. Even if it's not a problem with the language in its purest form, it is a problem with the ecosystem.
It is a Python problem because we praise Python for how portable it is, that it binds with every C library out there to provide the functionality we need at the performance we want and how easy it is to install with Pip. But then forget all the problems people get in during installing.

A fairer comparison would be comparing the ease of how to create and install a cross platform Go binary compared to how to distribute a Python application as a single package/pseudo-binary (I've been down that rabbithole and many others with Python packages in the past 15 years).

I'm a bit green on infrastructure & deployment but I don't quite get this. If your ML algorithm code is still Python how does deployment with Go make that much difference? It sounds like you're not replacing the Python ML code so why is this such a big deal?
I don’t think people who work in infrastructure currently will be surprised that Go is a better choice than Python for infra, but for those who are newer to the field of ML or only work on model development (vs deployment), it is likely surprising that a major part of production ML is best done in a language other than Python.
I don't buy this comment at all.

I've worked with hundreds of Data Scientists, many new to the industry and they all know that R and Scala are important and popular languages for ML.

Majority of Data Engineering today is using Spark, which is written in Scala and even when you write Python code using it you can't escape Java/Scala internals being exposed.

Is Scala really that prevalent? R & Python for sure.
> It sounds like you're not replacing the Python ML code so why is this such a big deal?

How would you write an article about moving to a hipster language then? :-)

I've re-read your reply a few times but I honestly don't get your point.
"in the land of the blind, the one-eyed man is king"

Golang is probably a step up from Python, but it's just that. There are a lot of issues with Golang. From the top of my head, lack of decent error handling (if err !=nil { return nil,err} ) or lack of decent polymorphism are the most annoying. There's a github repo dedicated to what's bugging people:

  https://github.com/ksimka/go-is-not-good
These two issues are usually brought up by people who haven't written a lot of Go.

In day to day work it's really not an issue.

>These two issues are usually brought up by people who haven't written a lot of C.

>In day to day work it's really not an issue.

Polymorphism and error handling, these are issues, address them. Go's a fine language, but don't dismiss actual real issues that have been addressed in other languages for 40+? years.

Status quo is good because it's status quo, sure.
I have written a lot of Go (hundreds of thousands of LOC), and appreciate Go's approach to both of these. I disagree that these are major issues.

A bigger issue, IMO, is that people coming to Go from other languages expect it to have similar features to what they're used to. Go is small and conceptually simple, but its ideas and philosophy are pretty different from most mainstream languages.

Go necessitates so much handrolling. Loops, concurrency patterns, resource management. You can't abstract over much, so you end up doing these things by hand constantly. Therefore you rely on brainpower, ad hoc linters (if one exists), and tests to not blow things up.

Go has its benefits and reasons to use it (especially the network effects around it). But when it comes to the three things I mention, Go is truly pathetic compared to a language like Haskell. It's amazing how much effort is wasted on those activities. I can see why a corporation would not care, but as an engineer I can do better with my time and effort for my personally-owned endeavors.

As an individual, many of Go's benefits aren't as useful. It's optimized for team settings, emphasizing consistency, readability, explictness, and low cognitive overhead, which are all major weaknesses of Haskell, and major reasons it's not widely used in industry.
For many applications, Go has higher cognitive overhead than Haskell. Modulo learning Haskell of course, but still.

Go is made to treat people as Resources. Full stop. That explains everything about it's benefits.

(comment deleted)
"I have written a lot of Go (hundreds of thousands of LOC)..."

Go only appeared about 10 years ago. The industry standard is 50 lines of production code a day. 50/day * 250 working days a year = 12.5k loc/year. So it would take you how many continuous years to write 200k LOC?

There's probably a very big difference between "industry standard" and working with a new language at a startup where a lot of things are written from scratch in a short amount of time. 50 lines of code on average for a year won't get you very far if you work on greenfield project.

The industry standard is probably very heavily biased by big corporate code bases where refactoring / architecture takes a lot more time than implementing the actual changes afterwards. Doesn't seem like a very useful calculation you are trying to do there to proof the previous poster wrong.

"There's probably a very big difference between" - that is precisely my point.

I have a lot of time playing ms flight sim and so obviously I know how to design airplanes is his argument.

Writing "greenfield" applications that no one (comparatively) is using is relatively easy. Creating systems that have four+ 9's availability, integrate with various disparate APIs, don't lose data, and,and etc is where, IMO, the learning is.

Once one has this experience, watching this micro services "movement" is, in my mind, related to global warming - like watching someone jump out of a plane with no parachute, smiling.

>Go is small and conceptually simple, but its ideas and philosophy are pretty different from most mainstream languages.

No, they aren't. They are the same as languages from 40 years ago: We have had channels, coroutines, and good garbage collectors from long, long ago. Plus the syntax is directly borrowed from Algol 68 (from 1968).

Go looks nice because it fills a niche (a high performance garbage collected language that is easier to learn than Java and provides faster compilation times than C++), but there's absolutely nothing different here.

For "pretty different from mainstream" see Forth, Prolog, Erlang, etc.

In some circles C has a better reputation than C++.

Surely you see that other people value other things than you do?

If you value simplicity check out the programming language zig.

https://ziglang.org/

I love C, loathe C++, but it is still valid issue that polymorphism in C is hard and error handling rely on convention ? (I know of Zig, Nim et al, I'm more oriented towards Rust this last year)
I write a lot of code in golang, and if anything, my views before using the language were even further cemented by even more flaws I found while using it.
Out of curiosity, would you share those flaws you have found?
I will hopefully later today, not able to type much now.
This is by no means a comprehensive list:

* no generics

* null pointers

* no compile time checked enums or sum types

* The golang time package is garbage

* golang interfaces are garbage, can't tag types with an interface without implementing a dummy method and hope that no other type implements it. Can't find what interfaces a type implements without an IDE

* profiling and debugging tools are nothing in front of JVM and .NET tools

* no const/immutablility

* no ternary operator, need to write 6 lines instead of a single line if it existed

* error handling is error prone and verbose

* no default method implementation in interfaces

* unit testing frameworks are sucky and rely on code gen, can't mock arbitrary types, but only by defining an interface

* that unnused variables and imports are compile time errors, makes it very annoying and time consuming to prototype or when debugging

* no private/public/etc modifier. if you want to change the visibility of a type, you need to rename it in all files it's listed in

* no incremental compilation (yes Java compiles faster in large projects where I only modify a handful of files, or even more)

* pervasive use of single letter identifiers in golang code bases

* pervasive use of int, which has a platform dependent size

* pervasive instantiation of structs by calling them structFoo { Field1: field1, Field2: field2, ... FieldN: fieldN }, which makes it easy to miss when a new field gets added. Rust and Zig solve this by making it required to specify all fields and values.

* defer works on the function scope, not the current scope

* defer doesn't handle functions that return errors, meaning many errors are missed

* no struct or file private, only package private

* the global scope is polluted with special functions like len, copy, delete, make, new, append, for no good reason

* golang imports don't support cyclic imports

* golang doesn't allow you to import a a specific function or struct from a package. You have you always qualify anything with the package name, which makes things verbose and awkward.

Thanks for the writing. I had noticed some of the things you wrote but not all (I am not a Go programmer though I keep track of what is happening there). Your post made me look a bit more into 'defer'. I did not expect it to be a source of errors.
No problem. As a side note, the Zig language does a much better job here, especially with defer. It works at the scope level (how it's supposed to be), and they have an `errdefer` construct used to call functions that return errors.
> no incremental compilation

Not true, unless you're using an esoteric definition of "incremental".

> imports don't support cyclic imports

Why on earth would you consider this bad?

The problem that I've ran into with the lack of cyclic imports is that I like to arrange the contents of packages logically. However sometimes in order to do this it would result in a circular dependency between two packages. So rather then getting a logical arrangement we have to refactor it into an artificial package to resolve this depenancy.
Circular imports are always a design error, no "logical" to it.
I like my code to look simple and behave as expected. Stability and "one right way to do it" are great language features.

I think this was why Python was able to overtake Perl, for example.

Well... there's also other side of the coin. There's value in visibility and lack of magic.

> lack of decent error handling (if err !=nil { return nil,err} )

Errors are in your face, instead of having exceptions performing invisible gotos to somewhere far up in call tree. Implicit error handling is more code, but your error handling is going to be much more robust.

> or lack of decent polymorphism...

Lack of polymorphism also means you don't have to guess about concrete types when reading code. When troubleshooting, you can see what's going on without going through whole inheritance tree.

Go encourages composition instead of inheritance. That's something I wish more C++ codebases would do as well. Composition makes code inherently more maintainable and easier to refactor.

Go tends to be easy to read and maintain. It does come with some cost. It's just a matter where your priorities lie. Software projects spend majority of their life as legacy, something that needs to be maintained.

>Composition makes code inherently more maintainable and easier to refactor.

My understanding of composition comes from interfaces in C#. In my experience composition is actually more difficult to code and refactor, because it leads to bloat from repeated code as you cannot simply inherit. Refactoring then can become tedious as you may need to manually edit every repeated code block. Whereas with inheritance I define a function once and override it where necessary.

Am I misunderstanding something?

Composition tends to lead to more code reuse. And to more code that's still usable in unpredictable future.

Over time your whole inheritance model often turns out to be no longer viable, when the basic assumptions made years ago are no longer valid.

Worse, because of all of the accumulated cruft, you might not even be able to change the shape of the monster.

I don't see that composition should be cause for repeated code blocks.
Composition should lead to clean components for reuse. You might need some extra references, but it is more explicit, static and flexible. Inheritance sounds great in theory, but is a hierarchical blind-alley, though usable for static designs that never change much.
In go, composition has a special feature where if you include another type anonymously in a struct, the methods of that type are automatically available for the struct as well. It is also possible to "override" these methods at the struct level and call the anonymous member directly.

So you can get some of the conveniences of inheritance. The behaviour is of course different, because the composed member has no way of knowing the identity of the including struct.

> Errors are on your face, instead of having exceptions performing invisible gotos to somewhere far up in call tree. Implicit error handling is more code, but your error handling is going to be much more robust.

Errors as values is a great idea. Errors as values without sum types or pattern matching, though, gives you twice the tedium and a tenth of the benefit, so once again Golang's slavish adherence to "worse is better" really just makes things...worse. The real problem here, though, is that those invisible gotos still exist: because of how limited Go's type system is, it's very easy to cause a panic with e.g. a nil pointer. Of course, the answer is to rigorously check the cases where a pointer could be nillable, but that trivially contradicts the idea that, when reading Go code, you can feel confident reasoning locally about its resilience if it handles all potential error values appropriately. IMO, this false sense of safety is even worse than just making exceptions first class citizens of the language, and it belies the notion that Go is especially maintainable.

Accessing nil would be a programming error, not normal errorhandling and code logic should inherently avoid the scenario, not hide or do coverup silently. If it breaks, let the process die.

The cost of simpler tools is you need to target better, simpler designs and refactor. This requires more from programmers but produces better code.

Try/catch vs explicitly checking error values is like implementing sandboxing with os-level isolation versus with programming discipline. Why one would prefer offering guarantees like "will never crash/panic" using the latter technique instead of a guaranteed catch-all is incomprehensible to me.
Performance and making logic explicit.
> Lack of polymorphism also means you don't have to guess about concrete types when reading code. When troubleshooting, you can see what's going on without going through whole inheritance tree.

Parametric polymorphism would mean you never have to even consider the type of the code. It's a freeing abstraction with plenty of very simple reference implementations. And it lends itself to composition over inheritance.

And sadly I doubt Go will ever manage to get it.

I would do it in python using one of the fast, modern ASGI servers like uvicorn.

Zero downtime model updates can be done using a redis cache to persist models.

In any case, that's a solved problem using haproxy and kubernetes.

Not sure why go has these advantages

If you do model inference in web server process it will be compute bound and lock up the web server, is there a preferred/clean way to req/rec or similar pass the jobs to second process and allow web server process to non-blocking wait for response?
Yes, use a dedicated job server/process like Celery.
I think, like TFA says, it comes down to ease of deployment for support tooling. A single executable is easier to distribute than a set of dependencies and a language runtime. These are tools that run outside containers to manage code that can run inside containers, where dependency management and isolation are easier. It makes total sense to me.
I would've chosen Erlang or Elixir for those reasons. Are we getting another Go package management solution this year? A pleasure to work with? I've been ditching Go for Nim recently. Other people seem to be enjoying Crystal. Rust is great, and coming down the road Zig looks excellent. I think Go has turned out to be a bit of a damp squib. Considering, unlike the other languages, it has Google behind it - unimpressed. After six years, I don't expect to be using it at all within the next year or two.
I think the real lesson here is to choose the language that works best for your team.

On my team we use Python and Scala. For network critical I/O stuff in Python, asyncio has worked out just fine for our needs. For massive CPU parallelism needs (at least in sporadic bursts), we've actually found that AWS/Lambda does pretty well.

Golang seems to be really polarizing. Most engineers on my team have tried Golang in the past, but haven't liked it, which is why we would never consider building anything on top of it. Everyone likes Python well-enough that it has kind of become the lingua franca for us.

Deployment is all based around containers or serverless/lambda, and we have a pretty standardized way of deploying these things by now. Just because a bunch of k8s tooling is written in Golang doesn't mean I need to rush out and write my stuff in Golang too.

Sounds like fairly generic deployment infrastructure that has nothing to do with machine learning.

But why pass up the opportunity to use a buzzword to get on the front page of HN?

This is a good point, and it’s something we think about a lot. In one sense, you can certainly think of Cortex as a tool for deploying/scaling/monitoring Python functions on AWS, but there are many ML-specific features that make it different.

It does things like prediction monitoring, and it supports models exported by ONNX and TF Serving. We also have designed Cortex to prioritize infrastructure needs specific to inference workloads (inference workloads are read only and memory hungry, for example). This is why we’ve prioritized things like GPU spot instances.

Our long-term plan includes more end-to-end ML workflows, including things like training, but for now we’re focused on getting model serving right.

I count at least 3: ML, Go, Python. Challenge: could they fit more in there under the title len limit?
Not reading carefully the title might make one think that you are doing ML with Go which by the content of the article you are obviously not. This is almost click-bait.
Anything is faster than CPython. Even PHP!
Great, another one of these articles, but this time I feel more confident in my usual reply, having been working in Go exclusively for a while.

> Implementing all of this functionality in Python may be doable with recent tools like asyncio, but the fact that Go is designed with this use case in mind makes our lives much easier.

This just makes me think about Armin Ronacher's article on back pressure but, sure, whatever.

> Building a cross-platform CLI is easier in Go

No, it isn't.

> The performance benefits of a compiled Go binary versus an interpreted language are also significant

Ah, yes, because performance is such a key feature of command line interfaces, as evidenced by bash and its outstanding performance in every benchmark.

> The Go ecosystem is great for infrastructure projects

And the reality discussed in this point would be different if docker wasn't written in Go. Had the docker developers chose anything else, this point would apply to that hypothetical language, so it isn't an inherent advantage of Go as a language.

> Go is just a pleasure to work with

No, it really, really isn't, but that's not the point.

This is ultimately the real reason they chose go: whoever made the original decision liked it and everything else is post-hoc rationalization.

Which is fine, most of this tends to be subjective.