Packages

Dataset management and caching for AI research benchmarks

Retired package: Deprecated - Use 0.5.0+

Current section

Files

Jump to
crucible_datasets lib dataset_manager loader.ex
Raw

lib/dataset_manager/loader.ex

defmodule CrucibleDatasets.Loader do
@moduledoc """
Unified dataset loading with automatic source detection and caching.
Supports loading from:
- HuggingFace datasets
- GitHub repositories
- Local files
- HTTP URLs
- CrucibleIR.DatasetRef structs
"""
alias CrucibleDatasets.{Cache, DataFiles, Dataset, DatasetDict, Format, IterableDataset}
alias CrucibleDatasets.Loader.{
Chat,
Code,
GSM8K,
HumanEval,
Math,
MMLU,
Preference,
Reasoning,
Rubric,
Vision
}
alias CrucibleIR.DatasetRef
require Logger
@dataset_sources %{
mmlu: {:huggingface, "cais/mmlu", "all"},
mmlu_stem: {:huggingface, "cais/mmlu", "stem"},
humaneval: {:huggingface, "openai/openai_humaneval", "default"},
gsm8k: {:huggingface, "openai/gsm8k", "main"},
math_500: {:huggingface, "HuggingFaceH4/MATH-500", "default"},
hendrycks_math: {:huggingface, "EleutherAI/hendrycks_math", "default"},
deepmath: {:huggingface, "zwhe99/DeepMath-103K", "default"},
polaris: {:huggingface, "POLARIS-Project/Polaris-Dataset-53K", "default"},
tulu3_sft: {:huggingface, "allenai/tulu-3-sft-mixture", "default"},
no_robots: {:huggingface, "HuggingFaceH4/no_robots", "default"},
hh_rlhf: {:huggingface, "Anthropic/hh-rlhf", "default"},
helpsteer3: {:huggingface, "nvidia/HelpSteer3", "preference"},
helpsteer2: {:huggingface, "nvidia/HelpSteer2", "default"},
ultrafeedback: {:huggingface, "argilla/ultrafeedback-binarized-preferences", "default"},
arena_140k: {:huggingface, "lmarena-ai/arena-human-preference-140k", "default"},
tulu3_preference: {:huggingface, "allenai/llama-3.1-tulu-3-8b-preference-mixture", "default"},
deepcoder: {:huggingface, "agentica-org/DeepCoder-Preview-Dataset", "default"},
open_thoughts3: {:huggingface, "open-thoughts/OpenThoughts3-1.2M", "default"},
deepmath_reasoning: {:huggingface, "zwhe99/DeepMath-103K", "default"},
feedback_collection: {:huggingface, "prometheus-eval/Feedback-Collection", "default"},
caltech101: {:huggingface, "dpdl-benchmark/caltech101", "default"},
oxford_flowers102: {:huggingface, "dpdl-benchmark/oxford_flowers102", "default"},
oxford_iiit_pet: {:huggingface, "dpdl-benchmark/oxford_iiit_pet", "default"},
stanford_cars: {:huggingface, "tanganke/stanford_cars", "default"}
}
@doc """
Load a dataset by name with automatic caching.
## Options
* `:version` - Specific version (default: "1.0")
* `:subset` - Subset name for multi-config datasets
* `:cache` - Use cache (default: true)
* `:sample_size` - Limit items (default: all)
* `:source` - Custom source path for local datasets
## Examples
iex> CrucibleDatasets.Loader.load(:mmlu_stem)
{:ok, %Dataset{name: "mmlu_stem", items: [...], ...}}
iex> CrucibleDatasets.Loader.load(:humaneval, sample_size: 50)
{:ok, %Dataset{name: "humaneval", items: [50 items], ...}}
iex> CrucibleDatasets.Loader.load("custom", source: "path/to/data.jsonl")
{:ok, %Dataset{name: "custom", ...}}
iex> ref = %CrucibleIR.DatasetRef{name: :mmlu_stem, split: :train, options: [sample_size: 100]}
iex> CrucibleDatasets.Loader.load(ref)
{:ok, %Dataset{name: "mmlu_stem", items: [...], ...}}
"""
@spec load(atom() | String.t() | DatasetRef.t(), keyword()) ::
{:ok, Dataset.t()} | {:error, term()}
def load(dataset_or_ref, opts \\ [])
def load(%DatasetRef{} = ref, extra_opts) do
# Convert DatasetRef to load options, merging with any extra opts
ref_opts = ref.options || []
opts = Keyword.merge(ref_opts, extra_opts)
load(ref.name, opts)
end
def load(dataset_name, opts) when is_atom(dataset_name) or is_binary(dataset_name) do
use_cache = Keyword.get(opts, :cache, true)
sample_size = Keyword.get(opts, :sample_size)
cache_key = build_cache_key(dataset_name, opts)
# Try to load from cache first
case use_cache && Cache.get(cache_key) do
{:ok, dataset} ->
{:ok, maybe_sample(dataset, sample_size)}
_ ->
with {:ok, source_spec} <- resolve_source(dataset_name, opts),
{:ok, dataset} <- fetch_and_parse(source_spec, dataset_name, opts),
{:ok, validated} <- Dataset.validate(dataset) do
cache_result =
if use_cache do
Cache.put(cache_key, validated)
else
:ok
end
case cache_result do
:ok -> {:ok, maybe_sample(validated, sample_size)}
{:error, reason} -> {:error, reason}
end
end
end
end
@doc """
Load a HuggingFace dataset by repo_id.
## Options
* `:config` - Dataset config/subset name
* `:split` - Split name (when nil, loads all splits into DatasetDict)
* `:streaming` - Return IterableDataset for lazy loading (requires split)
* `:revision` - Git revision (default: \"main\")
* `:token` - HuggingFace API token
## Returns
* `{:ok, Dataset.t()}` when split is specified and streaming is false
* `{:ok, DatasetDict.t()}` when split is nil
* `{:ok, IterableDataset.t()}` when streaming is true
"""
@spec load_dataset(String.t(), keyword()) ::
{:ok, Dataset.t() | DatasetDict.t() | IterableDataset.t()} | {:error, term()}
def load_dataset(repo_id, opts \\ []) when is_binary(repo_id) do
split = Keyword.get(opts, :split)
streaming = Keyword.get(opts, :streaming, false)
cond do
streaming and is_nil(split) ->
{:error, :streaming_requires_split}
streaming ->
load_dataset_streaming(repo_id, split, opts)
split ->
load_dataset_split(repo_id, split, opts)
true ->
load_dataset_all_splits(repo_id, opts)
end
end
@doc """
Invalidate cache for a dataset.
"""
@spec invalidate_cache(atom() | String.t()) :: :ok
def invalidate_cache(dataset_name) do
Cache.invalidate(dataset_name)
end
# Private helpers
defp build_cache_key(dataset_name, _opts) when is_atom(dataset_name) do
dataset_name
end
defp build_cache_key(dataset_name, _opts) when is_binary(dataset_name) do
{:local, dataset_name}
end
defp resolve_source(dataset_name, _opts) when is_atom(dataset_name) do
case Map.get(@dataset_sources, dataset_name) do
nil -> {:error, {:unknown_dataset, dataset_name}}
source -> {:ok, {dataset_name, source}}
end
end
defp resolve_source(dataset_name, opts) when is_binary(dataset_name) do
source = Keyword.get(opts, :source)
if source do
{:ok, {dataset_name, {:local, source}}}
else
{:error, {:missing_source, dataset_name}}
end
end
defp fetch_and_parse({dataset_name, source_spec}, _name, opts) do
case dataset_name do
name when name in [:mmlu, :mmlu_stem] ->
MMLU.load(dataset_name, opts)
:humaneval ->
HumanEval.load(opts)
:gsm8k ->
GSM8K.load(opts)
name when name in [:math_500, :hendrycks_math, :deepmath, :polaris] ->
Math.load(name, opts)
name when name in [:tulu3_sft, :no_robots] ->
Chat.load(name, opts)
name
when name in [
:hh_rlhf,
:helpsteer3,
:helpsteer2,
:ultrafeedback,
:arena_140k,
:tulu3_preference
] ->
Preference.load(name, opts)
:deepcoder ->
Code.load(:deepcoder, opts)
name when name in [:open_thoughts3, :deepmath_reasoning] ->
Reasoning.load(name, opts)
:feedback_collection ->
Rubric.load(:feedback_collection, opts)
name when name in [:caltech101, :oxford_flowers102, :oxford_iiit_pet, :stanford_cars] ->
Vision.load(name, opts)
_ ->
load_custom(dataset_name, source_spec, opts)
end
end
defp load_custom(name, {:local, path}, opts) do
case File.read(path) do
{:ok, content} ->
parse_jsonl(content, name, opts)
{:error, reason} ->
{:error, {:file_read_error, reason}}
end
end
defp load_custom(_name, _source, _opts) do
{:error, :unsupported_source}
end
defp parse_jsonl(content, name, _opts) do
items =
content
|> String.split("\n", trim: true)
|> Stream.map(&Jason.decode!/1)
|> Stream.with_index()
|> Enum.map(fn {raw, idx} ->
%{
id: "#{name}_#{idx}",
input: raw["input"] || raw["question"] || raw["text"],
expected: raw["expected"] || raw["answer"] || raw["label"],
metadata: Map.get(raw, "metadata", %{})
}
end)
dataset = Dataset.new(to_string(name), "1.0", items, %{source: "local"})
{:ok, dataset}
end
defp maybe_sample(dataset, nil), do: dataset
defp maybe_sample(dataset, size) when is_integer(size) do
sampled_items = Enum.take(dataset.items, size)
%{dataset | items: sampled_items, metadata: Map.put(dataset.metadata, :sampled, size)}
end
defp load_dataset_all_splits(repo_id, opts) do
with {:ok, %{config: config, splits: splits}} <- DataFiles.resolve(repo_id, opts) do
result =
splits
|> Enum.reduce_while({:ok, %{}}, fn {split, files}, {:ok, acc} ->
case load_dataset_from_files(repo_id, split, files, config, opts) do
{:ok, dataset} -> {:cont, {:ok, Map.put(acc, split, dataset)}}
{:error, reason} -> {:halt, {:error, reason}}
end
end)
case result do
{:ok, datasets} -> {:ok, DatasetDict.new(datasets)}
{:error, reason} -> {:error, reason}
end
end
end
defp load_dataset_split(repo_id, split, opts) do
split_str = to_string(split)
with {:ok, %{config: config, splits: splits}} <- DataFiles.resolve(repo_id, opts),
{:ok, dataset} <-
load_dataset_from_files(repo_id, split_str, splits[split_str], config, opts) do
{:ok, dataset}
end
end
defp load_dataset_streaming(repo_id, split, opts) do
split_str = to_string(split)
with {:ok, %{config: config, splits: splits}} <- DataFiles.resolve(repo_id, opts),
files when is_list(files) <- Map.get(splits, split_str),
true <- files != [] do
stream = build_stream(repo_id, files, opts)
iterable =
IterableDataset.from_stream(stream,
name: repo_id,
info: %{
source: "huggingface:#{repo_id}",
split: split_str,
config: config,
streaming: true
}
)
{:ok, iterable}
else
false -> {:error, {:split_not_found, split}}
nil -> {:error, {:split_not_found, split}}
{:error, reason} -> {:error, reason}
end
end
defp load_dataset_from_files(repo_id, split, files, config, opts) when is_list(files) do
case load_items_from_files(repo_id, files, opts) do
{:ok, items} ->
dataset =
Dataset.new(
repo_id,
"1.0",
items,
%{
source: "huggingface:#{repo_id}",
split: split,
config: config
}
)
{:ok, dataset}
{:error, reason} ->
{:error, reason}
end
end
defp load_dataset_from_files(_repo_id, _split, nil, _config, _opts) do
{:error, :no_files_found}
end
defp load_items_from_files(repo_id, files, opts) do
revision = Keyword.get(opts, :revision, "main")
token = Keyword.get(opts, :token)
results =
Enum.map(files, fn file ->
download_opts = [
repo_id: repo_id,
filename: file.path,
repo_type: :dataset,
revision: revision,
token: token,
extract: true
]
with {:ok, path} <- HfHub.Download.hf_hub_download(download_opts) do
parse_downloaded_path(path, file.format)
end
end)
merge_parse_results(results)
end
defp parse_downloaded_path(path, format_hint) do
paths =
if File.dir?(path) do
Path.wildcard(Path.join(path, "**/*"))
|> Enum.reject(&File.dir?/1)
else
[path]
end
results =
paths
|> Enum.map(fn file_path ->
format = if format_hint == :unknown, do: Format.detect(file_path), else: format_hint
parser = Format.parser_for(format)
if is_nil(parser) do
{:ok, []}
else
apply(parser, :parse, [file_path])
end
end)
merge_parse_results(results)
end
defp merge_parse_results(results) do
results
|> Enum.reduce_while({:ok, []}, fn result, {:ok, acc} ->
case result do
{:ok, items} -> {:cont, {:ok, acc ++ items}}
{:error, reason} -> {:halt, {:error, reason}}
end
end)
end
defp build_stream(repo_id, files, opts) do
files
|> Enum.map(&stream_file(repo_id, &1, opts))
|> Stream.concat()
end
defp stream_file(repo_id, file, opts) do
case file.format do
:jsonl -> stream_jsonl(repo_id, file.path, opts)
:parquet -> stream_parquet(repo_id, file.path, opts)
_ -> stream_fallback(repo_id, file, opts)
end
end
defp stream_jsonl(repo_id, path, opts) do
revision = Keyword.get(opts, :revision, "main")
token = Keyword.get(opts, :token)
{:ok, byte_stream} =
HfHub.Download.download_stream(
repo_id: repo_id,
filename: path,
repo_type: :dataset,
revision: revision,
token: token
)
Format.JSONL.parse_stream(byte_stream)
end
defp stream_parquet(repo_id, path, opts) do
Logger.warning("Parquet streaming is limited; loading file in batches.")
revision = Keyword.get(opts, :revision, "main")
token = Keyword.get(opts, :token)
batch_size = Keyword.get(opts, :batch_size, 1000)
{:ok, local_path} =
HfHub.Download.hf_hub_download(
repo_id: repo_id,
filename: path,
repo_type: :dataset,
revision: revision,
token: token,
extract: true
)
local_path
|> expand_data_paths()
|> Enum.map(&Format.Parquet.stream_rows(&1, batch_size: batch_size))
|> Stream.concat()
end
defp stream_fallback(repo_id, file, opts) do
case load_items_from_files(repo_id, [file], opts) do
{:ok, items} -> Stream.map(items, & &1)
{:error, _} -> Stream.map([], & &1)
end
end
defp expand_data_paths(path) do
if File.dir?(path) do
Path.wildcard(Path.join(path, "**/*"))
|> Enum.reject(&File.dir?/1)
|> Enum.filter(&(Format.detect(&1) in [:parquet, :jsonl, :json, :csv]))
else
[path]
end
end
end