For more details about these highlighted features, you can look at the release blogpost.
Below are the full release notes for this release.
Due to a bug introduced in CUDA 12.9.1, we are unable to complete full Windows wheel builds with this
version, as compilation of torch.segment_reduce()
crashes the build. Thus, we provide a wheel
without torch.segment_reduce()
included in order to sidestep the issue. If you need support
for torch.segment_reduce()
, please utilize a different version.
Due to binary size limitations, support for sm50 - sm60 architectures with CUDA 12.8 and 12.9 has
been dropped for the 2.8.0 release. If you need support for these architectures, please utilize
CUDA 12.6 instead.
NotImplementedError
instead of RuntimeError
(#155470)
Please update exception handling logic to reflect this.
In 2.7.0
try:
torch.nn.Hardshrink()(torch.randint(0, 5, (10,)))
except RuntimeError:
...
In 2.8.0
try:
torch.nn.Hardshrink()(torch.randint(0, 5, (10,)))
except NotImplementedError:
...
Added missing in-place on view check to custom autograd.Function
(#153094)
In 2.8.0, if a custom autograd.Function
mutates a view of a leaf requiring grad,
it now properly raises an error. Previously, it would silently leak memory.
class Func(torch.autograd.Function):
@staticmethod
def forward(ctx, inp):
inp.add_(1)
ctx.mark_dirty(inp)
return inp
@staticmethod
def backward(ctx, gO):
pass
a = torch.tensor([1.0, 2.0], requires_grad=True)
b = a.view_as(a)
Func.apply(b)
Output:
Version 2.7.0
Runs without error, but leaks memory
Version 2.8.0
RuntimeError: a view of a leaf Variable that requires grad is being used in an in-place operation
An error is now properly thrown for the out variant of tensordot
when called with a requires_grad=True
tensor (#150270)
Please avoid passing an out tensor with requires_grad=True
as gradients cannot be
computed for this tensor.
In 2.7.0
a = torch.empty((4, 2), requires_grad=True)
b = torch.empty((2, 4), requires_grad=True)
c = torch.empty((2, 2), requires_grad=True)
# does not error, but gradients for c cannot be computed
torch.tensordot(a, b, dims=([1], [0]), out=c)
In 2.8.0
a = torch.empty((4, 2), requires_grad=True)
b = torch.empty((2, 4), requires_grad=True)
c = torch.empty((2, 2), requires_grad=True)
torch.tensordot(a, b, dims=([1], [0]), out=c)
# RuntimeError: tensordot(): the 'out' tensor was specified and requires gradients, and
# its shape does not match the expected result. Either remove the 'out' argument, ensure
# it does not require gradients, or make sure its shape matches the expected output.
torch.compile Specialization of a tensor shape with mark_dynamic
applied now correctly errors (#152661)
Prior to 2.8, it was possible for a guard on a symbolic shape to be incorrectly
omitted if the symbolic shape evaluation was previously tested with guards
suppressed (this often happens within the compiler itself). This has been fixed
in 2.8 and usually will just silently "do the right thing" and add the correct
guard. However, if the new guard causes a tensor marked with mark_dynamic
to become
specialized, this can result in an error. One workaround is to usemaybe_mark_dynamic
instead of mark_dynamic
.
See the discussion in issue #157921 for more
context.
Version 2.7.0
import torch embed = torch.randn(2, 8192) x = torch.zeros(8192) torch._dynamo.mark_dynamic(x, 0) @torch.compile def f(embedding_indices, x): added_tokens_mask = torch.where(x > 10000, 1, 0) ei = torch.narrow(embedding_indices, 1, 0, x.size(0)) return ei.clone() f(embed, x)
Version 2.8.0
import torch embed = torch.randn(2, 8192) x = torch.zeros(8192) torch._dynamo.maybe_mark_dynamic(x, 0) @torch.compile def f(embedding_indices, x): added_tokens_mask = torch.where(x > 10000, 1, 0) ei = torch.narrow(embedding_indices, 1, 0, x.size(0)) return ei.clone() f(embed, x)Several config variables related to
torch.compile
have been renamed or removed
enable_cpp_framelocals_guard_eval
has changed to no longer have any effect (#151008).rocm.n_max_profiling_configs
is deprecated (#152341).rocm.ck_max_profiling_configs
androcm.ck_tile_max_profiling_configs
.autotune_fallback_to_aten
is deprecated (#154331).ATen
. Please add "ATEN"
tomax_autotune_gemm_backends
for the old behavior.use_mixed_mm
and mixed_mm_choice
are deprecated (#152071). Inductor now supports prologue fusion, so there is no need fordescriptive_names = False
is deprecated (#151481). Please use one of the other available"torch"
, "original_aten"
, or "inductor_node"
.custom_op_default_layout_constraint
has moved from inductor config to functorch config (#148104). Please reference it viatorch._functorch.config.custom_op_default_layout_constraint
instead oftorch._inductor.config.custom_op_default_layout_constraint
.emit_current_arch_binary
is deprecated (#155768).aot_inductor.embed_cubin
has been renamed to aot_inductor.embed_kernel_binary
(#154412).aot_inductor.compile_wrapper_with_O0
has been renamed to compile_wrapper_opt_level
(#148714).HigherOrderOperator
s (e.g. cond
), which will explicitly error out if alias/mutation among inputs and outputs is unsupported (#148953, #146658).
For affected HigherOrderOperator
s, add .clone()
to aliased outputs to address this.
Version 2.7.0
import torch @torch.compile(backend="eager") def fn(x): return torch.cond(x.sum() > 0, lambda x: x, lambda x: x + 1, [x]) fn(torch.ones(3))
Version 2.8.0
import torch @torch.compile(backend="eager") def fn(x): return torch.cond(x.sum() > 0, lambda x: x.clone(), lambda x: x + 1, [x]) fn(torch.ones(3))
guard_or_x
and definitely_x
have been consolidated (#152463)
We removed definitely_true
/ definitely_false
and associated APIs, replacing them withguard_or_true
/ guard_or_false
, which offer similar functionality and can be used to
achieve the same effect. Please migrate to the latter.
Version 2.7.0
from torch.fx.experimental.symbolic_shapes import definitely_false, definitely_true ... if definitely_true(x): ... if definitely_false(y): ...
Version 2.8.0
from torch.fx.experimental.symbolic_shapes import guard_or_false, guard_or_true ... if guard_or_false(x): ... # alternatively: if guard_or_false(torch.sym_not(y)) if not guard_or_true(y): ...torch.export
torch.export.export_for_inference
has been removed in favor of torch.export.export_for_training().run_decompositions()
(#149078)
Version 2.7.0
import torch ... exported_program = torch.export.export_for_inference(mod, args, kwargs)
Version 2.8.0
import torch ... exported_program = torch.export.export_for_training( mod, args, kwargs ).run_decompositions(decomp_table=decomp_table)Switched default to
strict=False
in torch.export.export
and export_for_training
(#148790, #150941)
This differs from the previous release default of strict=True
. To revert to the old default
behavior, please explicitly pass strict=True
.
Version 2.7.0
import torch # default behavior is strict=True torch.export.export(...) torch.export.export_for_training(...)
Version 2.8.0
import torch # strict=True must be explicitly passed to get the old behavior torch.export.export(..., strict=True) torch.export.export_for_training(..., strict=True)ONNX Default opset in
torch.onnx.export
is now 18 (#156023)
When dynamo=False
, the default ONNX opset version has been updated from 17 to 18. Users can set opset_version
to explicitly select an opset version.
Version 2.7
# opset_version=17 torch.onnx.export(...)
Version 2.8
# To preserve the original behavior torch.onnx.export(..., opset_version=17) # New: opset_version=18 torch.onnx.export(...)The
JitTraceConvertStrategy
has been removed (#152556)
Support for JIT traced and scripted modules in the ONNX exporter when dynamo=True
has been removed. You are encouraged to export an nn.Module directly, or create an ExportedProgram
using torch.export
before exporting to ONNX.
onnxscript>=0.3.1
is required for the dynamo=True
option (#157017)
You must upgrade onnxscript
to version 0.3.1 or higher for it to be compatible with PyTorch 2.8.
torch/types.h
include from Dispatcher.h
(#149557)
This can cause build errors in C++ code that implicitly relies on this include (e.g. very old versions of torchvision
).
Note that Dispatcher.h
does not belong as an include from torch/types.h
and was only present as a
short-term hack to appease torchvision
. If you run into torchvision
build errors, please
update to a more recent version of torchvision
to resolve this.
DLPack
to 1.0 (#145000)
As part of the upgrade, some of the DLDeviceType
enum values have been renamed. Please switch
to the new names.
Version 2.7.0
from torch.utils.dlpack import DLDeviceType
d1 = DLDeviceType.kDLGPU
d2 = DLDeviceType.kDLCPUPinned
...
Version 2.8.0
from torch.utils.dlpack import DLDeviceType
d1 = DLDeviceType.kDLCUDA # formerly kDLGPU
d2 = DLDeviceType.kDLCUDAHost # formerly kDLCPUPinned
...
NVTX3 code has been moved from cmake/public/cuda.cmake
to cmake/Dependencies.cmake
(#151583)
This is a BC-breaking change for the build system interface. Downstream projects that previously got NVTX3 through cmake/public/cuda.cmake
(i.e.. calling find_package(TORCH REQUIRED)
) will now need to explicitly configure NVTX3 support in the library itself (i.e. use USE_SYSTEM_NVTX=1
).
The change is to fix the broken behavior where downstream projects couldn't find NVTX3 anyway due to the PROJECT_SOURCE_DIR
mismatch.
Version 2.7.0:
-DUSE_SYSTEM_NVTX
would be able to find NVTX3 and torch::nvtx3
via PyTorch's cmake/public/cuda.cmake
logic.-DUSE_SYSTEM_NVTX
would encounter build errors with CUDA 12.8 or above.Version 2.8.0:
-DUSE_SYSTEM_NVTX
will not be able to find NVTX3 or torch::nvtx3
via PyTorch's cmake/public/cuda.cmake
. The downstream project now needs to explicitly find NVTX3 and torch::nvtx3 by implementing the same logic in PyTorch's cmake/Dependences.cmake
.-DUSE_SYSTEM_NVTX
will proceed building without NVTX unless another part of the build process re-enables NVTX.PyTorch 2.8 is the last release that will support GPU acceleration on MacOS Ventura. In the next
release (2.9), MacOS Sonoma (released in Sept. 2023) or above will be required to use the MPS
backend.
torch.ao.quantization
is deprecated and will be removed in 2.10 (#153892)
To migrate:
torch.ao.quantization.quantize
, torch.ao.quantization.quantize_dynamic
)
torchao
eager mode quantize_
.torchao
PT2E quantization.torch.ao.quantization.quantize_fx.prepare_fx
, torch.ao.quantization.quantize_fx.convert_fx
): use torchao
PT2E quantization (torchao.quantization.quantize_pt2e.prepare_pt2e
, torchao.quantization.quantize_pt2e.convert_pt2e
).Note that PT2E quantization has been migrated to torchao
(https://github.com/pytorch/ao/tree/main/torchao/quantization/pt2e). See pytorch/ao#2259 and https://docs.pytorch.org/ao/main/quick_start.html#pytorch-2-export-quantization for more details.
dynamo=False
(current default) option for torch.onnx.export
is deprecated (#152478, #155580)
The default will be dynamo=True
starting from PyTorch 2.9. You are encouraged to migrate to use the dynamo=True
option in torch.onnx.export
. This flag makes torch.export.export
the default export path, replacing TorchScript
.
To maintain the old behavior, set dynamo=False
explicitly. You are encouraged to also experiment with the fallback=True
option that will make the exporter fall back to the dynamo=False
path if there are errors.
nested_compile_region
(#156449)guard_filter_fn
(#150936)dont_skip_tracing
decorator to skip over most Dynamo skipfiles
rules (#150586)draft-export
, an export variant designed to consistently produce a graph and generate a debugging report of issues encountered during tracing (#152637, #153219, #149465, #153627, #154190, #155744, #150876, #150948, #151051, #151065, #150809, #151797)TorchBind
objects (#150196, #154265)aot_inductor.model_name_for_generated_files
for specifying model name (#154129)MPSInductor
: torch.compile
for Apple GPUs (#150121, #149342, #151449, #151754, #149687, #149180, #149221, #153598, #152788, #153787, #152214, #151152, #155891, #154578, #151272, #151288, #153997, #151871, #153362, #156566, #150661, #153582)Added new strategy draft_export
(#147529, docs) to provide debugging information upon data-dependent / constraint errors when obtaining an ExportedProgram
with torch.onnx.export
Added support for symbolic operators in the dynamo=True
export path (#148905, #149678, #150038, docs). Two operators torch.onnx.ops.symbolic
and torch.onnx.ops.symbolic_multi_out
are defined to allow you to create symbolic ONNX operators directly in your PyTorch models. You can use them in a forward
method:
def forward(self, x: torch.Tensor) -> torch.Tensor: # Optionally use is_in_onnx_export to control the behavior during onnx export if torch.onnx.is_in_onnx_export(): # Create a symbolic ONNX operator with the name "CustomOp" in the "custom_domain" domain. # The output tensor will have the specified dtype and shape return torch.onnx.ops.symbolic( "custom_domain::CustomOp", (x,), dict(attr_key="attr_value"), dtype=x.dtype, shape=x.shape, version=1, ) else: return xPython Frontend
torch.float4_e2m1fn_x2
dtype (#148791)TORCH_CUDA_ARCH_LIST
(#152715, #155314)bicubic
mode for torch::nn::functional::grid_sample
(#150817)no_implicit_headers
mode for load_inline()
on custom CUDA extensions (#149480)TCPStore
with clone and queuing features (#150966, #151045, #150969, #151485)getDefaultBackend
more fault tolerant without relying on exceptions (#149152)masterListenFd
in TCPStoreLibUvBackend
(#150215)TORCH_NCCL_USE_TENSOR_REGISTER_ALLOCATOR_HOOK
(#150682)global_rank
when group_rank
is used (#151373)ProcessGroupNCCL
via an unsafe API (#152496)needs_contiguous_strides
tag in functional collective (#153399, #153523)split_group
to work with non-nccl backends (#152175)new_subgroups()
by using new_subgroups_by_enumeration()
(#153843)ProcessGroupNCCL
(#153990)c10::Half
for gloo (#153862)get_process_group_ranks()
to accept group=None
(#154902)init_process_group
support index-only device id (#156214)ProcessGroup
(#151723)reduce_scatter
and ReduceOp::AVG
in ProcessGroupGloo
(#149781, #149869)ProcessGroupNCCL
(#152706)ibverbs
backend in gloo and enabled gloo CUDA when used with a backend that supports GPUDirect
(#153015, #153425, #153406)use_python_reducer
to C++ reducer (#152735)DistributedStateDict
(DSD)write_size
in planner write items (#149699)StridedShard
support uneven sharding (#150490)torch.cumsum
(#151071)DTensor
redistribute
fwd/bwd datatype conversion to enable SimpleFSDP
mixed precision training (#150740)torch.distributed.tensor.debug.visualize_sharding
(#152027)PrivateUse1
backend in FSDP collectives and device type to pre forward hook (#147260, #149487)set_reshard_after_forward
(#149103)reshard_after_forward=True
for root model and kept root unsharded when not specifying reshard_after_forward
(#154704, #155319)all_reduce_event
only if it's not CPU device (#150316)get_pipeline_order()
for Gpipe and 1F1B (#155935)ShardedTensor
and recalculated metadata from all_gather
(#152583)ParallelStyle PrepareModuleInputOutput
(#150372)__torch_function__
, and namedtuple
subclasses (#153150, #149792, #153982)reason
field to torch.compiler.disable
(#150341)lru_cache
warnings for functions in the top-level torch
namespace (#157718)aot_inductor.custom_ops_to_c_shims
and aot_inductor.custom_op_libs
: allow for specifying custom op C shim (#153968)max_fusion_buffer_group_pairwise_attempts
: limits fusions to specified node distance (#154688)cuda.cutlass_enabled_ops
: controls CUTLASS operation selection (#155770)triton.cudagraph_capture_sizes
: allows specifying certain shapes for which to capture CUDAGraphs; skips CUDAGraphs for other shapes (#156551)use_static_cuda_launcher
: enables launching compiled triton statically to improve cold start times (#148890)assume_unaligned_fallback_output
: allows inductor to track unaligned outputs (#150777)cuda.cutlass_tma_only
: controls whether or not to only use TMA-compatible kernels in CUTLASS (#152815)static_launch_user_defined_triton_kernels
: enables statically launching user defined triton kernels (#153725)precompilation_timeout_seconds
: controls the timeout on precompilation (#153788)disable_decompose_k
: disables new DecomposeK
GEMM Kernels (#154421)min_num_split
: sets the minimum number of splits in a split reduction (#155941)max_autotune_flex_search_space
: allows specifying the size of the search space for flex attention autotuning (#156307)LOG_AUTOTUNE_RESULTS
for autotune log (#156254)min
, max
, math.pow
) (#151348)pytree.register_dataclass
(#147752)jit.script
ed functions in export (#155180)==
(#150611)normalize_function
(#143689)graph_code_verbose_log
artifact for FX passes (#153775)fx.passes.split_module
to normalize input names (#157793)cross
(#154999)torch.special
operations as well as index_copy
, hardshrink
, rsub
, col2im
, and isin
(#149174, #149203 #149123, #149368, #149378, #149563, #149687, #149705, #149783, #149407/#149680, #150279, #151754, #153786, #154326, #155304, #156263, #155382, #154010, #149816, #152282, #156090, #150060, #151600, #155002, #154671)weight_norm
on CPU (#148878)dynamo=True
(#149901, #154596)Attention-23
and RotaryEmbedding-23
as native PyTorch ops (#156431, #156367, #154745)torch.scan
(#154513)group_norm
support from opset 21 (#152138)asdict
method to VerificationInfo
class (#151024)dynamic_shapes
behavior to use torch.export.dim.DYNAMIC
(#153065)sym_float
, sym_not
, sym_min
, sym_max
(#153200, #152111, #152196)TensorLR
variant for fused Adagrad on CPU (#153078)lr_lambda
type check in MultiplicativeLR
(#151973)torch.AcceleratorError
(#152023)Size.__radd__()
(#152554)get_default_device()
to also respect torch.device
context manager (#148621)mul
/ add
/ add_relu
and batch_norm2d
), qconv1d-relu fusion, and lowering pass (#151112, #152411, #152811, #150751, #149708)torch.fused_moving_avg_obs_fake_quant
on CUDA (#153699)cpp_extension
(#152432)mm
/bmm
/addmm
(#153262)PrivateUse1
extension (#149374)embed_cubin
and multi_arch_kernel_binary
options in AOTI for Intel GPU (#154514, #153924)UserDefineClass
(#155787)CMake-4.x
(#150203)gcc-12+
(#150847)/permissive-
flag (#149035)torch.norm
for scalar input (#144073)log_softmax
reduced-precision fp kernel (#156379)torch.backends.cuda.matmul.allow_fp16_accumulation
crash when using cuBLASLt (#153083)AsyncMM
on Blackwell (#153519)torch.cuda.MemPool
for multithreaded use-cases (#153356)sum()
on a default-constructed gamma / beta in layer_norm
(#156600)empty_cache
under mempool context (#158180)all_to_all
(#149485)group
input argument in new_subgroups()
(#152765, #153798)broadcast_object
util function (#155912)DDPOptimizer
issue on static tensor index (#155746)local_map
with multi-threading (#149070)new_local_tensor
in redistribute
be None case (#152303)TensorPipe
(#154382)gather
when a local tensor on certain ranks has zero elements (#150914)dict(mapping_proxy)
, and the FlexAttention HOP (#157754, #157515, #157519)lru_cache
method (#158689, #157308)TORCH_LOGS
argument is passed (#151678)aten.is_nonzero
(#149637), torch.bincount()
(#152497), aten.div
(#150874) slicing (#150104), and attn_mask
(#158618), aten.to
(#153972), scalar tensor construction (#154661)dynamic_shapes
spec for kwargs (#148772, #149528, #150103)functools.partial
(#153408), and higher order ops (#149295)None
inputs (#150515), math
module (#154643), call_torchbind
(#155647), and enums (#154821)update_constant_buffer
issue (#149243)model_package_loader
(#152334)AOTIModel
if they don't exist (#152692)ConstantFolding
(#153152)dot
and gemv
(#152676)torch.lobpcg
to compute same largest eigenvalue as scipy and np.linalg.eig
(#152789)ReducedPrecisionGemV
(#150949)2**32
+ element inputs, binary ops with inputs with different dtypes, ops with complex scalar inputs, cholesky
decomp, floor_divide
type promotion, index_kernel
with large inputs, lerp
with complex inputs, logit
with half/bfloat16 inputs, SDPA memory leak, torch.special.entr
, tri[ul]
, matrix inversion with N>1024
, and where
with non-contiguous cond
(#152479, #155183, #149233, #151176, #151282, #158239, #152371, #149974, #158237, #146754, #158867, #155184, #152204)load_state_dict
behavior for nn.LazyLinear
(#147599)onnx_program
callable (#151121)lr_scheduler
unexpectedly calls step()
when init argument last_epoch > -1
(#149312)CosineAnnealingWarmRestarts
resetting T_cur
(#151289)MixtureSameFamily
distribution (#151317)Wishart
or Uniform
distribution modifies constraints on the first (#154361)torch::utils::tensor_to_numpy
symbol (#154178)torch.[con]cat[enate]
to avoid crashing on empty inputs (#155460)torch.tensor
and torch.ops.aten.scalar_tensor
behavior (#158655)ScaledGEMM
(#149677)ScaledGEMM
(#152403)torch.is_vulkan_available()
on Mac (#155595)offset > 0
(#154495)torch.xpu.is_bf16_supported
to correctly report presence of Intel GPU (#152317)ELU(0)
with the cheaper definition (#155765)SubsetRandomSampler
by iterating over list instead of tensor (#149126)cpp.use_small_dequant_buffer
to use a small dequant buffer for WOQ int4 GEMM (#156395)torch.dot
with float16/bfloat16 (#152799)LayerNorm
, mm
/ bmm
, sum
/ prod
reductions, arithmetic ops,linear
, and cumsum
/ cumprod
(#152010, #150541, #150566, #147644, #149730, #152781, #152210, #157494)torch.tensordot
when contracting to a scalar (#145936)softmax
, NLLLoss
, in-place sum, max pooling backward / reductions on NHWCHipSparseLT
to further accelerate semi-structured (e.g. 2:4) sparsity (#150578)addmm
, baddmm
to reduce oneDNN integration overhead on Intel GPU (#153051)ctx.save_for_backward
is important in note about extending autograd (#153005)torch.autograd.graph.saved_tensors_hooks
to avoid refcycle (#153049)torch.amin
and torch.amax
(#155071)NCCLConfig
with QOS variable (#151821)get_default_backend_for_device
(#158236)ignored_params
docstring and added unit tests (#149074)Dims
and ExportGraphSignature
(#156262, #156244)torch.linalg.norm()
's ord argument of +2 & -2 (#155148)nn.RNN
, nn.functional
loss functions, interpolate
saturate cast behavior, ConvTranspose2d
stride
/ output_size
arguments, and register_full_backward_hook
(#155123, #153620, #148436, #151304, #150819, #150609, #151785)nn.Sequential
and nn.LazyModuleMixin
(#147304, #150596)nn.modules.padding
and AvgPoolND
(#155618, #152680)LRScheduler
s (#149189)CosineAnnealingLR
to accurately reflect its recursive learning rate schedule (#152936)Adafactor
documentation (#145209)load_state_dict
hint doc about invoke order work with lr_scheduler
(#149942)torch.Library
's kind
have no default value to be consistent with the code (#149390)requires_grad=True
in tensor.to()
(#150913)cdist
param description (#151178)Example:
and not Example::
in docs (#153978)as_strided()
docs (#149146)keepdim
param optional description (#151197)torch.trapezoid
docs (#151190)out_dtype
arg for torch GEMM operations (#151704)torch.min()
, torch.max()
, torch.all()
, and torch.any()
(#152658)torch.triu_indices
, torch.tril_indices
dtype description (#150749)torch.equal
description (#149618)get_default_qat_qconfig
in prepare_qat_fx
docs (#155100)nccl_version
and thread name/id, for flight record in PGNCCL (#150356, #150513, #151048, #152648, #155142, #155754)new_subgroups()
for Non-Divisible World Sizes (#154124)get_backend()
with more details (#141796)FlatParamHandle
(#151336)rpc_init
to CPython (#154325)torch.distributed.run
option to provide destination for event logging (#155268)TracingContext
(#149294)detect_attr_assignment
(#151824)AOTInductor
runtime API for Intel GPU (#153929)stable::Tensor is_contiguous
API (#156228)lr_scheduler.py
(#151219)step()
with default value (#153367)setup-python
from for Mac tests (#155698)RetroSearch is an open source project built by @garambo | Open a GitHub Issue
Search and Browse the WWW like it's 1997 | Search results from DuckDuckGo
HTML:
3.2
| Encoding:
UTF-8
| Version:
0.7.4