log in | about 
 

Understanding Performance of Long-Document Ranking Models through Comprehensive Evaluation and Leaderboarding

Attention folks working on LONG-document ranking & retrieval! We found evidence of a very serious issue in existing long-document collections, most importantly MS MARCO Documents. It can potentially affect all papers comparing different architectures for long document ranking.

Do not get us wrong: We love MS MARCO: It has been a fantastic resource that spearheaded research on ranking and retrieval models. In our work we use MS MARCO both directly as well as its derivative form: MS MARCO FarRelevant.

Yet MS MARCO (and similar collections) can have substantial positional biases which not only "mask" differences among existing models, but also prevent MS MARCO trained models from performing well on some other collections.

This is not a modest degradation where performance drops roughly to BM25 level (as we observed, e.g., in our prior evaluation).. It can be a dramatic drop in accuracy down to the random baseline level.

We found a substantial positional bias of relevant information in standard IR collections, which include MS MARCO Documents and Robust04. However, judging from other paper results a lot of other collections are affected.

Because relevant info tends to be in the beginning of a doc we often do not need a special long-context model to do well in ranking and retrieval. The so-called FirstP model (truncation to < 512 tokens) can do well (see the picture, we do not plot FirstP explicitly, but all the numbers are relative to FirstP efficiency or accuracy):

So how do we prove positional bias is real? There is no definitive evidence, but we provide several evidence pieces. In doing so we introduce a NEW synthetic collection MS MARCO Far Relevant which does not have relevant passages in the beginning.

  1. On MS MARCO we trained 20+ rank models (including two FlashAttention-based models) and observed all of them to barely beat FirstP (in rare cases models underperformed FirstP);.
  2. We analyzed positions of relevant passages and found these to be skewed to the beginning;
  3. We 0-shot tested & then fine-tuned models on MS MARCO Far Relevant.

Unlike standard collections where we observed both little benefit from incorporating longer contexts & limited variability in model performance (within a few %), experiments on MS MARCO FarRelevant uncovered dramatic differences among models.

  1. To begin with, FirstP models performed roughly at the random-baseline level in both 0-shot and fine-tuning scenarios (see the picture below).

  2. Second, simple aggregation models including MaxP and PARADE Attention had good zero-shot accuracy, but benefited little from fine-tuning.

  3. In contrast, most other models had poor 0-shot performance (roughly at a random baseline level), but outstripped MaxP by as much as 13-28% after fine-tuning. Check out the lines connecting markers in left and right columns:

Best models? PARADE models were the best and markedly outperformed FlashAttention-based models, Longformer as well as chunk-and-aggregate approaches. PARADE Attention performed best on standard collection (in most cases) and in the zero-shot transfer model on MS MARCO FarRelevant. However, among fine-tuned models on MS MARCO Far Relevant, the best was PARADE Transformer (with a randomly initialized aggregator model).

This paper is an upgrade of 2021 preprint enabled by a great collaborator David Akinpelu as well as by the team of former CMU (MCDS) students. David, specifically, did a ton of work including implementation of recent baselines.

Paper link.

Code/Data link



Electric highway delayed: Why I haven't bought a fully electric car

About ten years ago I wrote that the electric highway might be nearer than I initially thought. I am still a big fan of electrification. In 2007, I bought a Camry Hybrid, which made me a relatively early adopter: only about 350 thousand hybrids were sold in the US that year, compared to about 16 million new cars and light trucks overall. In other words, hybrids represented only about 2% of the market.

Things have changed quite a bit. In the second quarter of 2026, regular hybrids accounted for 16% of new US light-duty vehicle sales. If we add plug-in hybrids and fully electric cars, the share was 24%. However, fully electric cars alone accounted for only 6% of new sales. And because new cars make up only a small fraction of cars already on the road, fully electric cars were only about 2% of the registered US light-duty vehicle fleet in 2024.

There are currently about 289 million light vehicles on US roads, while annual new registrations are roughly 16 million. Even in the completely unrealistic scenario where 100% of new vehicles became electric tomorrow, dividing one number by the other gives us about 18 years just to replace the current fleet once. This is clearly a lower bound rather than a realistic transition time: only a small fraction of new cars are fully electric today, and the average US light vehicle is now 12.8 years old. I would therefore not be surprised to see gasoline-only and hybrid cars on American roads in substantial numbers for another 30-40 years, perhaps even longer. Thus, despite enormous progress, we still have a long way to go before full electrification.

This brings me to a much more practical question. Our old Camry has recently started to give out. We replaced quite a few things, and, somewhat amusingly, the hybrid battery was not even the most expensive repair. We decided to keep the Camry as a modestly used second car, which we actually need from time to time. But we also needed a replacement for our primary car. So the question was: Should we finally buy a fully electric one?

I was actually quite tempted, but the ultimate family decision was to buy a plug-in hybrid instead. This seems to be a rather nice compromise. For our local driving, this means that the car can function nearly all the time as an electric vehicle. On a long trip, however, we do not need to plan the trip around fast chargers or worry much about finding a working charger in the right place.

This latter issue was probably decisive for me. The charging infrastructure has improved enormously, but I do not think it is ready enough that I want to depend on it during every long trip. This is not merely range anxiety. In the In the 2025 J.D. Power public charging study, 14% of EV owners reported at least one visit to a public charger where they were unable to charge. This was actually a substantial improvement compared to the previous year, but 14% is still a surprisingly large number when you are traveling with your family and need the charger to work. The most common reason for a failed charging visit was that the charger was out of service or did not work properly.

There is also an interesting technological complication. The battery technology we have now may not be the battery technology we have several years from now. Toyota is targeting 2027-2028 for commercialization of its all-solid-state batteries, and Nissan is targeting fiscal 2028. If these plans actually work at mass-production scale, solid-state batteries may substantially change the calculus: manufacturers are promising higher energy density, longer range and much faster charging. For example, Toyota has discussed a target of 10 minutes or less for charging from 10% to 80% together with improved driving range. Of course, these are manufacturer targets rather than capabilities of cars one can buy today. Better batteries, however, do not automatically build charging stations, increase their availability, or make broken chargers work.

Thus, somewhat paradoxically, I remain quite optimistic about electric cars while deciding not to buy one. I suspect that my next new car will finally be fully electric. But cars last a long time in our family. If the new plug-in hybrid lasts anything like the old Camry, we may be driving it for another 15-20 years. By then, I expect the electric highway to be considerably closer than it is today.



LLM Skills: A New Source of Technical Debt

I have some thoughts related to the recent craze to automate everything with agentic skills. On a plus side, I think it can be very useful when done correctly. On the negative side, it is often done incorrectly and only adds more technical debt. Instead of solving systemic issues first, people are just slapping an AI band-aid in the hope to stop a gushing artery.

Using an agent (and essentially an LLM) as the primary orchestrator for deterministic workflows sacrifices many of the properties that decades of good software engineering have optimized for. We lose predictability, reproducibility, testability, debuggability, and the ability to reason formally about system behavior. More subtly, we often sacrifice encapsulation: instead of components exposing explicit, versioned interfaces, the LLM needs to understand implementation details and prompt conventions. This increases coupling between components, makes the overall system less coherent, and turns what should be local implementation changes into workflow-wide prompt adjustments. One should not forget that natural language is imprecise and ambiguous so it is in principle impossible to define complex workflows accurately.

Humans often lack understanding of how the agent interoperates with different components and it is hard to debug or predict how LLM will behave. For workflows where an execution sequence can be well defined (which includes quite a few workflows), the agent's role should be much narrower. It should primarily act as a thin interface layer: gather user intent, fill slots, perform lightweight validation, and invoke a well-defined sequence of deterministic scripts or services. The workflow itself should be explicit, documented, version-controlled, and independently executable. Mixing code-snippets and natural language commands in a single SKILLS.md file feels like an engineering antipattern. It tightly couples deterministic scripts with a non-deterministic sequence of steps defined by the agents and makes debugging extremely difficult. What makes debugging difficult is not just the nondeterminism. It's that the control flow becomes implicit rather than explicit.

Agent-based orchestration has an important operational requirement. If something goes wrong (whether due to an LLM hallucination, a provider outage, a rate limit from Codex or Claude Code, or simply an unexpected edge case) a human engineer should be able to resume or rerun the workflow manually by invoking the same documented scripts. The system should remain inspectable and recoverable because the deterministic backbone exists independently of the agent. In my experience, this requirement is frequently overlooked.

Agents still have valuable roles outside the critical execution path where their use is more flexible. They can assist with debugging failures, investigating unexpected outcomes, suggesting fixes, summarizing logs, or helping operators understand complex system state. In those cases, occasional mistakes are acceptable because a human is already in the loop. Moreover, when something goes wrong, it is better to obtain a quick (though possibly not 100% precise) assessment of the situation rather than waste days to obtain a correct one. But the production workflow itself should remain deterministic whenever the problem domain permits it.



Why I Built mtasklite: The Parallel Map Loop Done Right

For the last seven years, I kept re-implementing the same pattern: A parallel map loop that divides the work among several processes or threads..

My very first attempts were built on Python’s standard tools, e.g., multiprocessing.map, and I later explored pqdm (the “Parallel TQDM” library) to simplify the code (as well as to visualize progress on long-running loops) when workers were stateless. Those solutions did work but always felt incomplete and required repeated boilerplate. Much later did I explore tools like Dask and Ray (see examples in the end of the post) and found that, for this specific pattern, they still required a lot of complexity and infrastructure for something that should be simple: A simple, streaming, stateful, parallel map loop with accurate progress tracking and minimal boilerplate — that actually works everywhere.

In 2024, I finally distilled the pattern into a tiny library: mtasklite.

Below is the motivation, the flaws of existing approaches, and what makes mtasklite unique.

Common Pain Points with Existing Tools

Even for simple parallel loops, many libraries fall short:

1. Stateful workers with flexible initialization
Most map APIs assume stateless functions. When workers must load models or context before processing, solutions become awkward or require elaborate hacks.

2. True streaming input/output
Many frameworks materialize outputs in bulk rather than yielding results as soon as they are ready. This breaks bounded-memory pipelines.

3. Size-aware streaming for progress tracking for limited-size input/output
Tools that process fixed-size input arrays often fail to expose an output with a usable len(...). Without a known length, progress bars like tqdm can’t estimate remaining time.

4. Notebook compatibility
Some parallel tools (e.g., Ray actors, certain ways of using multiprocessing) behave unpredictably in Jupyter/IPython environments.

5. Boilerplate and cognitive overhead
Great libraries like Dask and Ray are powerful, but for a simple map pattern they introduce clusters, schedulers, plugins, actors, futures, and daemons (see examples in the end of the post).

6. Lightweight API
Many real workloads don’t need a full distributed system — just a clean, streaming, parallel map loop.

A more detailed discussion of these limitations follows later.

What Makes mtasklite Different

Minimal Boilerplate
You don’t need:

- Clusters or schedulers
- Actors or plugins
- Futures or object references, which need to be checked for completion
- Background daemons or services

Stateful Workers with Flexible Initialization
Workers can:

- Initialize once with stateful setup
- Receive per-worker init arguments
- Maintain local state across tasks

This is essential for real workloads — model loading, database connections, tokenizers, GPU context, etc.

True Streaming Input + Output
mtasklite works with:

- Any input iterator
- True streaming of results

⟶ Outputs appear as soon as they’re ready
⟶ Memory stays bounded (unless requested)
⟶ No buffering of the entire input and output set

This is crucial for real world pipelines that process huge datasets.

Size-Aware Output for Progress Bars
When the input iterator has a known length, the mtasklite output iterator provides a length function as well. This enables easy and accurate progress bars (tqdm, etc.).

Works Everywhere
No matter the environment:

- Jupyter Notebooks
- Python REPL
- Scripts
- Interactive environments

mtasklite avoids features that frequently misbehave in notebooks or interactive shells.

Lightweight and Elegant API
The API stays close to familiar Python map semantics:

  1. for out in parallel_map(f, xs):
  2. ...

Easy Migration from pqdm
If you’re already a pqdm user — the “Parallel TQDM” library built on concurrent.futures + tqdm — switching to mtasklite is very easy. The API is nearly 100% compatible, and there’s a dedicated migration guide describing a couple of tiny but necessary differences.

That means you can enjoy:

- Per-worker initialization
- True streaming output
- Sized iterators for progress bars
- Notebook-friendly behavior

…without rewriting your existing loops.

A Motivating Example

Below is a (somewhat contrived) example of a stateful, class-based, worker that computes a square root. It is implemented as a regular class with the constructor and a __call__ function with the only nuance: It is decorated using the @delayed_init decorator. This decorator "wraps" the actual object inside a shell object, which only memorizes object's initialization parameters. An actual instantiation is delayed till a worker process (or thread) starts.

  1. from mtasklite import delayed_init
  2. from mtasklite.processes import pqdm
  3.  
  4. @delayed_init
  5. class Square:
  6. def __init__(self, worker_id):
  7. print(f"Initializing worker {worker_id}")
  8.  
  9. def __call__(self, x):
  10. return x * x
  11.  
  12. inputs = [1, 2, 3, 4, 5]
  13.  
  14. with pqdm(inputs, [Square(0), Square(1), Square(2), Square(3)]) as out:
  15. for y in out:
  16. print(y)

This provides:

- Stateful worker initialization
- Streaming output
- Sized iterator compatible with progress bars
- Bounded memory
- Works in notebooks and scripts
- No external infrastructure

Why Other Tools Did Not Fit

Below is a concise explanation of why other popular tools don’t satisfy this pattern without complexity.

multiprocessing + tqdm
Using multiprocessing and tqdm together requires a lot of repetition and manual work:

- Worker init routines are limited
- Integrating with progress bars often requires imap/imap_unordered patterns
- You must manage iterator lengths manually to drive tqdm correctly
- Behavior can be unpredictable in notebooks

Yes, you can piece together something yourself — but you end up rewriting the same wrapper logic repeatedly. Below is an example of how cumbersome it is:

  1. from tqdm.auto import tqdm
  2. from multiprocessing import Pool
  3.  
  4. def init_worker(proc_id):
  5. global WORKER_ID
  6. WORKER_ID = proc_id
  7. print(f"[Worker init] ID = {WORKER_ID}")
  8.  
  9. def worker_task(a):
  10. # Use global WORKER_ID if needed
  11. return a * a
  12.  
  13. if __name__ == "__main__":
  14. proc_ids = [0,1,2,3]
  15. input_vals = list(range(20))
  16. chunksize=1
  17. result = []
  18. with Pool(processes=len(proc_ids), initializer=init_worker, initargs=(proc_ids.pop(0),)) as pool:
  19. # map is a blocking call that return the result only when all input is processed
  20. #for ret_val in tqdm(pool.map(worker_task, input_vals, chunksize)):
  21. for ret_val in tqdm(pool.imap(worker_task, input_vals, chunksize)):
  22. result.append(ret_val)
  23. print(result)

Dask
Dask is powerful for distributed computation, but for this pattern it is an overkill (which, again, requires additional boilerplate).Dask excels at DAGs and large dataframes, but doesn’t treat this lightweight map pattern as a first-class use case. In this specific example it was also 10x slower compared to mtasklite (with a 6-core Intel Mac).

  1. ​​from dask.distributed import LocalCluster, Client
  2. from tqdm.auto import tqdm
  3.  
  4. class SquareActor:
  5. def __init__(self, proc_id):
  6. import multiprocess as mp
  7. print(f"Initialized process {mp.current_process()} with argument = {proc_id}")
  8. self.proc_id = proc_id
  9.  
  10. def compute(self, a):
  11. # Method to call for each task
  12. return a * a
  13.  
  14. def run_with_square_actors(input_arr, proc_ids):
  15. # Start cluster & client via context managers
  16. with LocalCluster(n_workers=len(proc_ids), processes=True) as cluster:
  17. with Client(cluster) as client:
  18. # Create one actor per proc_id
  19. actor_futures = [
  20. client.submit(SquareActor, pid, actor=True)
  21. for pid in proc_ids
  22. ]
  23. actors = [f.result() for f in actor_futures]
  24.  
  25. # Dispatch work to actors by calling the method `.compute(...)`
  26. results = []
  27. for i, val in enumerate(tqdm(input_arr)):
  28. actor = actors[i % len(actors)]
  29. # This returns an actor future; .result() waits for execution
  30. results.append(actor.compute(val).result())
  31.  
  32. return results
  33.  
  34. if __name__ == "__main__":
  35. input_arr = list(range(20))
  36. proc_ids = [0, 1, 2, 3]
  37. result = run_with_square_actors(input_arr, proc_ids)
  38. print(result)

Ray
Ray’s actor model solves stateful workers elegantly, but:

- Ray runtime initialization often conflicts with notebooks
- There’s no built-in streaming map API; you write your own loops
- API feels distributed-system-first rather than lightweight

Ray is a powerful platform, but still not “simple map loop first.” It can also be quite heavy-weight (due to start up overhead). In this specific example, it is about 40x slower compared to mtasklite:

  1. #!/usr/bin/env python
  2. import ray
  3. from collections import deque
  4. from tqdm.auto import tqdm
  5.  
  6. ray.init()
  7.  
  8. @ray.remote
  9. class Worker:
  10. def __init__(self, proc_id):
  11. import multiprocess as mp
  12. print(f"Initialized worker {proc_id} in process {mp.current_process()}")
  13. self.proc_id = proc_id
  14.  
  15. def compute(self, x):
  16. return x * x
  17.  
  18. def ray_stateful_stream_map(inputs, proc_ids, worker_qty=8):
  19. """
  20. A generator that streams outputs as they complete,
  21. using stateful Ray actors with proc_ids.
  22. Args:
  23. inputs: An iterable of inputs.
  24. proc_ids: A list of init args for each actor.
  25. worker_qty: Max number of workers
  26. """
  27. actors = [Worker.remote(pid) for pid in proc_ids]
  28. pending = deque()
  29. it = iter(inputs)
  30.  
  31. # Submit initial batch
  32. for _ in range(min(worker_qty, len(proc_ids))):
  33. try:
  34. val = next(it)
  35. except StopIteration:
  36. break
  37. # round-robin
  38. actor = actors[len(pending) % len(actors)]
  39. pending.append(actor.compute.remote(val))
  40.  
  41. while pending:
  42. # Wait for the next ready future
  43. ready, _ = ray.wait(list(pending), num_returns=1)
  44. ref = ready[0]
  45. pending.remove(ref)
  46. yield ray.get(ref)
  47.  
  48. # Submit more work
  49. try:
  50. val = next(it)
  51. actor = actors[len(pending) % len(actors)]
  52. pending.append(actor.compute.remote(val))
  53. except StopIteration:
  54. pass
  55.  
  56. result = []
  57. for ret_val in tqdm(ray_stateful_stream_map(range(1, 21), proc_ids=[0,1,2,3], worker_qty=8)):
  58. result.append(ret_val)
  59. print(result)

Conclusion

I did not set out to build another parallelism library — I just kept rewriting this same code for six years:

- Stateful workers with init
- Streaming outputs
- Sized outputs for progress bars
- Minimal boilerplate
- Works everywhere

Once I distilled it into a tiny, composable API, it became clear this pattern deserved its own package.

That package is mtasklite — a tiny, elegant, minimal toolkit for real-world parallel map loops.

GitHub:
https://github.com/searchivarius/py_mtasklite



A Curious Case of Remarkable Resilience to Gradient Attacks via Fully Convolutional and Differentiable Front End with a Skip Connection

I recently finished, perhaps, my nerdiest computer science paper so far and it was accepted by TMLR: A Curious Case of Remarkable Resilience to Gradient Attacks via Fully Convolutional and Differentiable Front End with a Skip Connection. This work was done while I was at the Bosch Center of AI (outside my main employment at Amazon), but I was so busy that I had little time to put it in writing. I submitted the first draft only last year, and it required a major revision, which I was able to finish only in July. It is not a particularly practical paper—I accidentally discovered an interesting and puzzling phenomenon and decided to document it (with a help of my co-authors).

The following AI-generated summary, I believe, provides an accurate description of our work:

The paper presents a fascinating case study on how a seemingly innocuous modification to a neural network can drastically alter its perceived robustness against adversarial attacks. I accidentally discovered a simple yet surprisingly effective technique: prepending a frozen backbone classifier with a differentiable and fully convolutional "front end" model containing a skip connection. This composite model, when trained briefly with a small learning rate, exhibits remarkable resistance to gradient-based adversarial attacks, including those within the popular AutoAttack framework (APGD, FAB-T).

Here's a breakdown of the paper's core elements, methodology, and findings in more detail:

1. The Curious Phenomenon:

  • We observed that adding a differentiable front end to a classifier significantly boosted its apparent robustness to adversarial attacks, particularly gradient-based ones.

  • This robustness occurred without significantly sacrificing clean accuracy (accuracy on non-adversarial, original data).

  • The effect was reproducible across different datasets (CIFAR10, CIFAR100, ImageNet) and various backbone models (ResNet, Wide ResNet, Vision Transformers).

  • The training recipe for the front end (small learning rate, short training duration) was stable and reliable.

2. The Front End Architecture (DnCNN):

  • The front end used is based on DnCNN (Denoising Convolutional Neural Network), a fully convolutional architecture with skip connections.

  • Skip connections are crucial for training deep networks. They help the gradient flow more smoothly during backpropagation, preventing vanishing gradients.

  • DnCNN is differentiable. Unlike some adversarial defense strategies that rely on non-differentiable components (like JPEG compression) to disrupt gradient-based attacks, DnCNN allows gradients to be computed throughout the entire network.

  • We argue that the success of this method is especially surprising because the use of a skip connection is actually expected to improve gradient flow, not to mask gradients.



Pages

Subscribe to RSS - srchvrs's blog