Yet again, we’re at the end of our internship season, and so it’s time to summarize what the interns were up to!
实习季节再次结束,是时候总结一下实习生们都在做什么了!

This year, I was recommended a real bumper crop of exciting projects to include. It’s kind of crazy how many great intern projects are out there. To mention a few that I’m not going to have time to cover in detail:
今年,我推荐了许多令人兴奋的项目。真是疯狂,现在有这么多优秀的实习生项目。以下是一些我无法详细介绍的例子:

  • Annie Hu spent a big chunk of her summer investigating, implementing, and optimizing different neural net sequence models, trying out a variety of compilation techniques and toolchains.
    Annie Hu 花费了她夏天的大部分时间研究、实现和优化不同的神经网序列模型,尝试了各种编译技术和工具链。
  • Aster Oransoy added build priorities to our build systems’ shared action execution service, so you can ensure low-priority builds don’t slow down high-priority ones.
    Aster Oransoy 为我们的构建系统的共享动作执行服务添加了构建优先级,这样你可以确保低优先级构建不会影响高优先级构建。
  • Allen Pei wrote a quickcheck -like system for creating automated tests of trading systems by generating randomized sequences of market events, along with shrinking heuristics for creating minimal test cases.
    Allen Pei 编写了一个类似 quickcheck 的系统,用于通过生成随机的市场事件序列来创建交易系统的自动化测试,并附带了一种用于创建最小测试用例的缩减启发式算法。
  • Evan Thompson wrote an LSP for our inline CSS syntax extension which includes a CSS validator that found tons of instances of invalid CSS in our applications.
    Evan Thompson 编写了一个 LSP,用于我们的内联 CSS 语法扩展,其中包括一个 CSS 验证器,在我们的应用程序中发现了大量无效 CSS 的实例。
  • Zhibo Chen added a generic form of optional arguments to OCaml, so that it can use other types than the traditional OCaml option type (including more efficient representations) for optional values.
    Zhibo Chen 为 OCaml 添加了可选参数的通用形式,使其可以使用除传统 OCaml option 类型之外的其他类型(包括更高效的表示)来处理可选值。
  • Conor Kennedy added predicate pushdown to our internal data warehouse system to do filtration before it gets to the full query engine, and even wrote a mini query planner for analyzing filter expressions to derive narrower key ranges.
    Conor Kennedy 为我们的内部数据仓库系统添加了谓词下推功能,以便在数据到达完整查询引擎之前进行过滤,甚至还编写了一个小型查询计划器来分析过滤表达式,以推导出更精确的键范围。
  • Joe Cutler worked on using JIT-ing to make our HardCaml simulator fast enough to be competitive with Verilator, but with much better start-up times.
    乔·卡特勒致力于使用 JIT 技术使我们的 HardCaml 模拟器速度足够快,可以与 Verilator 竞争,但启动时间要好得多。

And those are just the ones I felt like I could explain in a handful of words each!
而且这些只是我觉得能用几句话解释清楚的那些!

As usual, I picked just three projects to go into in more detail. In particular:
和往常一样,我挑选了三个项目进行更详细的介绍。具体来说:

  • Leo Gagnon wrote a (sometimes dramatically) more efficient evaluator for JSQL, our internal SQL dialect that we use for lots of different user-facing tools.
    Leo Gagnon 编写了一个(有时戏剧性)更高效的 JSQL 评估器,JSQL 是我们用于许多不同用户工具的内部 SQL 方言。
  • Aryan Khatri built a new version of our OCaml torch bindings that leverage OxCaml’s new features for controlling memory management to build bindings that clean up tensors safely and deterministically.
    Aryan Khatri 构建了我们 OCaml torch 绑定库的新版本,利用 OxCaml 的新特性控制内存管理,以构建能够安全、确定性清理张量的绑定。
  • Anthony Li wrote a library for managing memory across processes within our trading systems via ref-counting, making it possible to more efficiently and safely ship data across the process boundary.
    Anthony Li 编写了一个库,通过引用计数管理我们交易系统中的进程间内存,使得跨进程边界传输数据更加高效和安全。

Let’s dive in!让我们深入探讨!

Faster (J)SQL evaluation更快的(J)SQL 评估

We use a lot of SQL at Jane Street, both in standard Postgres (or similar) databases floating around, and for accessing our own homegrown analytics-oriented data warehouse software.
我们在 Jane Street 大量使用 SQL,无论是在标准的 Postgres(或类似)数据库中,还是用于访问我们自研的分析导向型数据仓库软件。

Over time, we came to realize that SQL was sufficiently well-known internally that we wanted to use it beyond the context of databases, as a general language for filtering and transforming tabular data. This could be useful in all sorts of contexts: web UIs, data visualization tools, trading-systems configuration tools, etc.
随着时间的推移,我们逐渐意识到 SQL 在内部已经足够知名,我们希望将其用于数据库之外,作为过滤和转换表格数据的一般语言。这可以在各种环境中发挥作用:Web UI、数据可视化工具、交易系统配置工具等。

The problem with this idea is… which version of SQL should you use? Every database you look at has its own charmingly unique SQL dialect that’s almost but not quite the same as all the others.
这个想法的问题在于……应该使用哪个版本的 SQL?你查看的每个数据库都有自己的独特 SQL 方言,几乎与其他所有方言相似但又略有不同。

We decided to deal with this by (I know, I know) building our own dialect of SQL called JSQL. We’ve built a bunch of tools for using JSQL, including parsers, translators to other SQL dialects, web-UI components, and a collection of different in-memory evaluators for computing the results of a JSQL expression without invoking a traditional database at all.
我们决定通过(我知道,我知道)构建自己的 SQL 方言——JSQL 来解决这个问题。我们为使用 JSQL 构建了一系列工具,包括解析器、转换为其他 SQL 方言的转换器、Web UI 组件,以及一系列不同的内存评估器,用于计算 JSQL 表达式的结果,而无需调用传统数据库。

Our evaluators started out very simple, doing little more than walking though a collection of rows and one-by-one evaluating whether they passed or failed a WHERE clause. Over time, we’ve built multiple evaluators with different performance properties, including incremental evaluators.
我们的评估器最初非常简单,基本上只是遍历一系列行,逐个评估它们是否满足 WHERE 子句。随着时间的推移,我们开发了多个具有不同性能特性的评估器,包括增量评估器。

That said, none of our evaluators were all that sophisticated, and in particular, none of them made use of indexing. Leo Gagnon’s project was to change that!
话说回来,我们所有的评估者都不够复杂,特别是他们都没有使用索引。Leo Gagnon 的项目就是改变这一点!

The idea was that when presented with data that’s in an indexed container, like a Map.t or Hashtbl.t, to be able to use that indexing to more efficiently filter down to the data you need. So, if you have a SELECT statement where the WHERE clause contains:
这个想法是,当面对一个索引容器中的数据,比如 Map.tHashtbl.t ,能够利用这个索引来更高效地筛选出你需要的数据。所以,如果你有一个 SELECT 语句,其中 WHERE 子句包含:

author = "Dijkstra" AND publication_year > 1980

and the underlying data is contained in, say, a Paper.t list String.Map.t (a map from author names to lists of their papers), Leo’s evaluator would have to:
而底层数据包含在,比如一个 Paper.t list String.Map.t (一个从作者名字到他们论文列表的映射),Leo 的评估器将不得不:

  • determine that we only care about things under the key "Dijkstra",
    确定我们只关心键 "Dijkstra" 下的内容,
  • use an O(log n) Map.find to get the resulting Paper.t list,
    使用一个 O(log n) Map.find 来获取结果 Paper.t list
  • use List.filter on the resulting much smaller list to select the papers with publication_year > 1980
    在结果中较小的列表上使用 List.filter 来选择带有 publication_year > 1980 的论文

Which is way more efficient than walking over the entire map.
这比遍历整个映射要高效得多。

Getting this done involved a bunch of steps!
完成这件事涉及多个步骤!

  • Building a selection type that represented the possible over-approximations of the range of keys that would be needed to satisfy a given query.
    构建一个 selection 类型,它表示满足给定查询所需的键的范围的可能过度近似。
  • Writing code to extract and optimize the selection for a given query.
    编写代码来提取和优化给定查询的 selection
  • Writing code to specialize the execution of the selection to the backing store for the data. For example, the selection type tracks when ranges of queries are in scope. The Map.t type supports efficient range queries, but the Hashtbl.t type doesn’t, so you need different execution strategies depending on which you use to store your data.
    编写代码以专门针对数据存储的后端来优化选择执行。例如, selection 类型会跟踪查询范围内的范围。 Map.t 类型支持高效的范围查询,但 Hashtbl.t 类型不支持,因此根据你选择的数据存储类型,需要不同的执行策略。
  • Supporting multi-index data-structures, like our Immutable_indexable_bag. This involved building selection heuristics that help us pick the most efficient index to use.
    支持多索引数据结构,如我们的 Immutable_indexable_bag 。这涉及构建选择启发式算法,帮助我们选择最有效的索引。

And, of course, benchmarking.
当然,还有基准测试。

The results of that benchmarking were pretty promising. We ran some sample queries over 3.8 million rows of test data, comparing a linear scan over an array versus an index-optimized scan over a Map.t.
那次基准测试的结果相当有前景。我们在 380 万行测试数据上运行了一些样本查询,比较了在数组上进行线性扫描与在 Map.t 上使用索引优化扫描的速度。

This first query shows a ~700x speedup, since it lets us zoom in on just the MSFT trades, ignoring everything else.
第一个查询显示了约 700 倍的加速,因为它允许我们只关注 MSFT 交易,忽略其他所有内容。

SELECT * WHERE und = "MSFT US" AND event_date > "2025-01-01"::date
+----------------------------------+--------------------+
| aggregator_name                  | average_time       |
+----------------------------------+--------------------+
| jsql-aggregations-eval           | 15.844514478s      |
| jsql-indexed-aggregations-eval   | 21.939788ms        |
+----------------------------------+--------------------+

This second query is more complicated, in that it requires us to do a scan over a range of values, but we still get a ~30x speedup here.
这个第二个查询更复杂,因为它需要我们对一个值范围进行扫描,但我们在这里仍然获得了约 30 倍的加速。

SELECT * WHERE
  (und = "MSFT US" OR (und >= "AAPL US" AND und < "AMZN US"))
  AND event_date > "2025-01-01"::date
+--------------------------------+--------------------+
| aggregator_name                | average_time       |
+--------------------------------+--------------------+
| jsql-aggregations-eval         | 37.056874003s      |
| jsql-indexed-aggregations-eval | 1.324532585s       |
+--------------------------------+--------------------+

Despite this being a pretty algorithmic and performance-oriented project, a lot of the challenges turned out to be about API design, and getting all of this work done with a codebase that was simple and readable, and presented a convenient API to users.
尽管这是一个相当注重算法和性能的项目,但许多挑战最终都变成了关于 API 设计,以及如何在一个简单易读的代码库中完成所有工作,并向用户展示一个便捷的 API。

Better Torch bindings 更好的 Torch 绑定

We use PyTorch a lot as part of our machine learning efforts, and as you might expect, most of that work is done in Python. But sometimes, we want to drive PyTorch from OCaml, which we do using ocamltorch, originally written by some years back.
我们大量使用 PyTorch 作为我们机器学习工作的一部分,正如你可能预期的,大部分这项工作都是在 Python 中完成的。但有时,我们希望从 OCaml 驱动 PyTorch,我们使用 ocamltorch 来实现这一点,ocamltorch 最初是由 Laurent Mazare 多年前编写的。

But OCaml is in some ways an awkward match for PyTorch, because OCaml manages memory using a tracing GC, in contrast to Python, which uses a refcounting GC.
但 OCaml 在某些方面与 PyTorch 不太匹配,因为 OCaml 使用追踪式垃圾回收管理内存,而 Python 则使用引用计数式垃圾回收。

A lot of ink has been spilled on the tradeoffs between refcounting and tracing, but one clear difference is around the determinism of collection. With a tracing GC, it’s hard to know when the memory you’ve allocated will be reclaimed. With refcounting, your object will be collected the moment you drop your last reference to it.
关于引用计数和追踪之间的权衡已经有很多讨论,但一个明显的区别在于收集的确定性。使用追踪式垃圾回收时,很难知道你分配的内存何时会被回收。使用引用计数时,你的对象会在你释放最后一个引用的瞬间被回收。

This determinism comes in handy when you’re using your collector for managing things other than main memory, like precious GPU memory. This is a plot of GPU memory usage over time doing one forward and backward pass on a batch, then some sampling, then 3 more batches, written naively with ocamltorch.
当你使用收集器来管理除主内存之外的其他事物,比如珍贵的 GPU 内存时,这种确定性就很有用。这是一个 GPU 内存使用情况的图表,展示了在一个批次上执行一次前向和后向传递,然后进行一些采样,接着是 3 个更多批次的操作,用 ocamltorch 以简单的方式编写。

Torch memory use when running with a tracing GC

This behavior is pretty awful! We’re holding on to tensors we just don’t need anymore, which is basically intolerable.
这种行为相当糟糕!我们还在保留那些不再需要的张量,这基本上是无法忍受的。

You’d deal with this in ocamltorch by carefully calling Gc.full_major () after each batch and each token sampled, to force the GC to recognize that the memory is unused and reclaim it. That gives you the desired memory behavior:
在 ocamltorch 中,你会通过在每个批次和每个采样的 token 后仔细调用 Gc.full_major () ,来强制 GC 识别内存未被使用并回收它。这给了你所需的内存行为:

Torch memory use when you deallocate tensors in the right place

but it’s a poor solution, since the calls to the GC are expensive, and there’s no discipline to help you make sure you put them in the right place.
但它是一个糟糕的解决方案,因为调用 GC 很昂贵,而且没有纪律来帮助你确保将它们放在正确的位置。

Aryan’s project was to build a better API for ocamltorch that provided a safe and efficient discipline for managing tensor memory, leveraging some of the new features of OxCaml, a set of extensions to OCaml that have been developed at Jane Street.
Aryan 的项目是构建一个更好的 ocamltorch API,它提供了一种安全高效的纪律来管理张量内存,利用了 OxCaml 的一些新特性,OxCaml 是一组在 Jane Street 开发的 OCaml 扩展。

The basic idea is to introduce a way of marking a scope of allocation for a tensor, using this with_rc_scope function, where “rc” is short for “reference count”:
基本思想是引入一种标记张量分配范围的方法,使用这个 with_rc_scope 函数,其中“rc”是“引用计数”的缩写:

val with_rc_scope : (unit -> 'a) @ local -> 'a

The idea is that the body of the closure passed to this function acts as a scope, and that any tensors allocated within it will have their refcounts decremented when the function ends.
这个想法是,传递给这个函数的闭包的体充当一个作用域,并且在该函数结束时,其中分配的任何张量都会使其 refcounts 递减。

To make this all work, we use OxCaml’s local mode to make sure that tensors can’t escape their scope. In particular, any function that allocates a tensor will allocate it as a local value:
为了让这一切正常工作,我们使用 OxCaml 的 local 模式来确保张量不会逃逸出其作用域。特别是,任何分配张量的函数都会将其作为局部值进行分配:

val ( + ) : t @ local -> t @ local -> t @ local

This prevents the allocated value from being returned from the closure passed to with_rc_scope.
这可以防止分配的值从传递给 with_rc_scope 的闭包中返回。

Here’s a worked example of how you might use this in practice.
这里是一个实际使用该功能的示例。

let vs = Var_store.create ~name:"vs" () in
let opt = Optimizer.sgd vs ~learning_rate:1e-3 in
let model = Model.init vs in
for index = 1 to 100 do
  Tensor.with_rc_scope (fun () ->
    Optimizer.zero_grad opt;
    let ys_ = Model.forward model xs in
    let loss = Tensor.(mean (square (ys - ys_))) in
    Tensor.backward loss;
    Optimizer.step opt)
done;

The full API is a bit more complicated than just that. The system has support for nested scopes, which is needed to support many of the idioms that are used in practice for both training and inference workflows on GPUs. As part of that, there is some special support for returning tensors from an inner scope to an outer scope in a controlled way that doesn’t violate the reference counting rules.
完整的 API 比这要复杂一些。该系统支持嵌套作用域,这对于在 GPU 上支持训练和推理工作流中实际使用的许多惯用方法来说是必需的。作为其中的一部分,有一些特殊的支持用于以受控方式将张量从内部作用域返回到外部作用域,而不会违反引用计数规则。

The project itself involved a lot of experimentation at the API level, to design an API that was easy to use and understand and that also captured the memory-use patterns we run into in practice. The project also had an interesting performance-engineering aspect to it: removing all of the now-unnecessary GC invocations made it easier to understand and identify further inefficiencies (like unnecessary synchronizations between the CPU and GPU) that were harder to see amongst the performance mess created by the full_major invocations.
这个项目本身涉及大量 API 层面的实验,旨在设计一个易于使用和理解,同时又能捕捉我们在实践中遇到的内存使用模式的 API。该项目还有一个有趣的性能工程方面:移除所有现在不再必要的 GC 调用,使得更容易理解和识别进一步的低效之处(如 CPU 和 GPU 之间不必要的同步),而这些在由 full_major 调用所造成的性能混乱中更难发现。

We have more ideas about how to extend and improve these interfaces, but we already expect the new APIs to be quite useful in their current form. This is part of our open-source code, so once the new code is released, you’ll be able to find it here.
我们还有更多关于如何扩展和改进这些接口的想法,但我们已经预期新的 API 在其当前形式下将非常有用。这是我们的开源代码的一部分,一旦新代码发布,你将能够在这里找到它。

Ref-counted objects in shared memory共享内存中的引用计数对象

At Jane Street, we have lots of performance-sensitive trading systems that gather complex information over the course of their execution, and then periodically serialize pieces of that data over a shared-memory channel to another process.
在 Jane Street,我们有很多对性能敏感的交易系统,这些系统在其执行过程中收集复杂信息,然后定期通过共享内存通道将数据的一部分序列化到另一个进程。

This is generally a pretty good approach, but it has its limitations. Serialization always has a cost, but here it’s made worse by the fact that the data we want to send is complex nested data with shared structure between messages. As a result, serializing the data can involve serializing the same sub-structures over and over.
这是一种总体上相当不错的方法,但它有其局限性。序列化总是有代价的,但在这里,由于我们想要发送的数据是具有共享结构的复杂嵌套数据,因此代价更加严重。结果,序列化数据可能涉及反复序列化相同的子结构。

Anthony Li’s project was to build a library supporting a very different – and much more efficient – approach.
Anthony Li 的项目是构建一个支持非常不同——而且效率高得多——方法的库。

The idea is to get rid of the serialization and deserialization altogether, and to just pass pointers to the values in question instead. This requires that the space of objects in question is visible to both processes, so it means we need to allocate those objects within a shared memory segment.
想法是彻底消除序列化和反序列化的过程,而直接传递相关值的指针。这要求相关对象的空间对两个进程都是可见的,因此这意味着我们需要在共享内存段中分配这些对象。

We already have support for managing pools of objects in a shared memory segment, so this sounds easy enough at first glance. But the tricky bit is figuring out when you can recycle one of your pooled objects.
我们已经有支持在共享内存段中管理对象池的功能,所以乍一看这似乎很简单。但棘手之处在于确定何时可以回收池中的一个对象。

We can’t rely on OCaml’s ordinary GC for this because the data resides in a shared-memory segment between two processes, each with their own GC. And anyway, we don’t want to be churning the garbage collector in a latency-sensitive trading system.
我们不能依赖 OCaml 的普通垃圾回收器来实现这一点,因为数据位于两个进程之间的共享内存段中,每个进程都有自己的垃圾回收器。而且,我们也不想在一个对延迟敏感的交易系统中频繁地运行垃圾回收器。

Instead, Anthony’s project was to use a tried-and-true technique for this: reference counting.
相反,安东尼的项目是使用一种经过验证的技术:引用计数。

Safer refcounting through modes通过模式实现更安全的引用计数

Reference counting is tricky to integrate into a language like OCaml that doesn’t have it designed in from the start. There are really three invariants you need to get right for this system to work:
将引用计数集成到像 OCaml 这样从一开始就没有设计它的语言中是棘手的。要使这个系统正常工作,你需要正确处理三个不变式:

  • There are no data-races on the refcounts or the objects themselves
    refcounts 或对象本身没有数据竞争
  • Refcounts are incremented every time a new reference is created
    引用计数每次创建新引用时都会增加
  • Refcounts are decremented every time a reference is destroyed
    引用计数每次销毁引用时都会减少

But how do we ensure that these rules are followed when they’re not natively enforced by the runtime? This is a bit like the problem Aryan ran into with reference counting in PyTorch, and again, the solution is to leverage the system of modes to ensure the necessary invariants.
但我们如何确保这些规则在没有运行时原生强制执行时得到遵守?这有点像 Aryan 在 PyTorch 中遇到的问题,参考计数问题,同样,解决方案是利用模式系统来确保必要的不变式。

We’ll need different modes at different times, so in order to manage this, we’re going to have a special handle object o Handle.t that guards access to the underlying object (of type o). We can both use modes to protect the use of the handle itself, and the handle can release the object o with specific modal types under specific circumstances.
我们需要在不同时间使用不同的模式,为了管理这一点,我们将使用一个特殊的句柄对象 o Handle.t 来保护对底层对象(类型为 o )的访问。我们可以使用模式来保护句柄本身的使用,同时句柄可以在特定情况下以特定的模式类型释放对象 o

That’s all a bit abstract, so let’s talk about the details:
这一切有点抽象,让我们谈谈细节:

Eliminating data races 消除数据竞争

There are really two data-race questions to handle here: one is about the refcounts, and the other is about the actual objects being managed. For the refcounts, an atomic compare-and-set operation can be used to manage them in a safe way, so that’s pretty simple, and doesn’t require anything from the mode system.
这里确实有两个数据竞争问题需要处理:一个是关于引用计数的,另一个是关于实际被管理的对象。对于引用计数,可以使用原子比较并设置操作来安全地管理它们,所以这很简单,也不需要模式系统提供任何支持。

The mutability of the objects is more complicated, because the rules are different at different times. The objects must be mutable on initialization, since they have to be filled in at that point. But once you have multiple readers of the object, you really need them to not change. It turns out we can leverage OxCaml’s visibility mode axis, which include immutable, read, and read_write modes.
对象的可变性更为复杂,因为规则在不同时间会有所不同。对象必须在初始化时是可变的,因为它们需要在这个时间点被填充。但是一旦对象有多个读取者,你确实需要它们保持不变。事实证明我们可以利用 OxCaml 的可见性模式轴,包括 immutablereadread_write 模式。

Specifically:具体来说:

  • During initialization, we expose the value under the read_write mode (which is the default), so the data in the object can be set. Notably, at this point, we’re guaranteed there’s only one reference to the object in question.
    在初始化期间,我们以 read_write 模式(即默认模式)暴露值,以便对象中的数据可以被设置。值得注意的是,在这个时间点,我们保证所讨论的对象只有一个引用。
  • When reading, we expose objects under the read mode. This way, multiple readers (even across processes) can access the same object without fear of a race.
    当阅读时,我们在 read 模式下暴露对象。这样,多个读取者(甚至跨进程)可以无惧竞争地访问同一个对象。

Notably, once an object’s reference count goes back to zero, it can again be the subject of an initialization, so it can again be exposed read_write.
值得注意的是,一旦对象引用计数回到零,它就可以再次成为初始化的客体,因此可以再次在 read_write 模式下暴露。

Another interesting aspect of this is that when we release the underlying values, we do so under the local mode, to prevent the value from escaping its intended scope. As such, what we’re implementing is analogous to borrow-checking in Rust.
这个有趣的方面是,当我们释放底层值时,我们在 local 模式下进行,以防止值逸出其预期作用域。因此,我们实现的是类似于 Rust 中的借用检查。

Managing increments and decrements管理增量和减量

The key invariant here is that people don’t just go about duplicating handles without incrementing the associated reference count. To ensure this, each Handle.t is created under the unique mode, and all operations that use handles require that they be provided uniquely.
这里的关键不变量是人们不会随意复制句柄而不增加相关的引用计数。为确保这一点,每个 Handle.t 都是在 unique 模式下创建的,并且所有使用句柄的操作都要求它们被唯一提供。

This guarantees that all handles that are used are held uniquely, and so if you want to refer to the handle in multiple places, an explicit copy function must be called. And, critically, that copy function increments the reference count.
这保证了所有使用的句柄都是唯一持有的,因此如果你想在多个地方引用该句柄,必须调用显式的复制函数。并且,关键的是,该复制函数会增加引用计数。

There’s also a free operation that consumes a handle and decrements the reference count. And a way of sending a handle to another process, at which point the sending handle is consumed, and a receiving handle is created, without changing the reference count.
还有一个 free 操作会消耗一个句柄并减少引用计数。还有一种将句柄发送到另一个进程的方法,此时发送的句柄被消耗,并创建一个接收句柄,而不会改变引用计数。

Anthony’s library is complete, and the team is now working it into our production systems. We hope that this will be a library that’s useful to multiple teams across the firm.
Anthony 的库已经完成,团队现在正在将其集成到我们的生产系统中。我们希望这将是一个对公司在多个团队都很有用的库。

Join us! 加入我们!

If this sounds like a fun way to spend your summer, you should apply to our internship program. Jane Street interns get a chance to solve fun and challenging problems that have real impact. I hope this post gives you a sense of that!
如果这听起来像是你度过夏天的有趣方式,你应该申请我们的实习项目。Jane Street 的实习生有机会解决有趣且具有挑战性的问题,这些问题具有实际影响。我希望这篇文章能让你感受到这一点!